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
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  buildSessionTimelineNodes,
3
3
  buildUsageChartGroups,
4
+ buildUsageHeatmap,
5
+ buildUsageHourlyHeatmap,
4
6
  isSessionQueryEnabled
5
7
  } from '../logic.mjs';
6
8
  import { SESSION_TRASH_PAGE_SIZE } from './app.constants.mjs';
@@ -56,6 +58,24 @@ function formatUsageEstimatedCost(value, options = {}) {
56
58
  }).format(numeric);
57
59
  }
58
60
 
61
+ function formatSignedUsageSummaryNumber(value) {
62
+ const numeric = Number(value);
63
+ if (!Number.isFinite(numeric) || numeric === 0) {
64
+ return '0';
65
+ }
66
+ const sign = numeric > 0 ? '+' : '-';
67
+ return `${sign}${formatUsageSummaryNumber(Math.abs(numeric))}`;
68
+ }
69
+
70
+ function formatSignedUsageEstimatedCost(value, options = {}) {
71
+ const numeric = Number(value);
72
+ if (!Number.isFinite(numeric) || numeric === 0) {
73
+ return '$0.00';
74
+ }
75
+ const sign = numeric > 0 ? '+' : '-';
76
+ return `${sign}${formatUsageEstimatedCost(Math.abs(numeric), options)}`;
77
+ }
78
+
59
79
  function formatUsageRangeLabel(range, t) {
60
80
  const normalized = typeof range === 'string' ? range.trim().toLowerCase() : '7d';
61
81
  if (typeof t === 'function') {
@@ -335,9 +355,13 @@ export function createSessionComputed() {
335
355
  const pinnedMap = (this.sessionPinnedMap && typeof this.sessionPinnedMap === 'object')
336
356
  ? this.sessionPinnedMap
337
357
  : {};
338
- if (Object.keys(pinnedMap).length === 0) {
358
+ const sortMode = typeof this.sessionSortMode === 'string'
359
+ ? this.sessionSortMode.trim().toLowerCase()
360
+ : 'time';
361
+ if (sortMode !== 'hot' && Object.keys(pinnedMap).length === 0) {
339
362
  return list;
340
363
  }
364
+ const now = Date.now();
341
365
  let hasPinned = false;
342
366
  const decorated = list.map((session, index) => {
343
367
  const key = session ? this.getSessionExportKey(session) : '';
@@ -349,12 +373,28 @@ export function createSessionComputed() {
349
373
  if (isPinned) {
350
374
  hasPinned = true;
351
375
  }
352
- return { session, index, pinnedAt, isPinned };
376
+ const updatedAtMs = session ? Date.parse(session.updatedAt || '') : NaN;
377
+ const safeUpdatedAtMs = Number.isFinite(updatedAtMs) ? updatedAtMs : 0;
378
+ const messageCount = session && Number.isFinite(Number(session.messageCount))
379
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
380
+ : 0;
381
+ const totalTokens = session && Number.isFinite(Number(session.totalTokens))
382
+ ? Math.max(0, Math.floor(Number(session.totalTokens)))
383
+ : 0;
384
+ const ageHours = safeUpdatedAtMs > 0 ? Math.max(0, (now - safeUpdatedAtMs) / 3600000) : 1e9;
385
+ const activity = Math.sqrt(Math.max(1, totalTokens || (messageCount * 120)));
386
+ const hotScore = activity / (1 + (ageHours / 24));
387
+ return { session, index, pinnedAt, isPinned, safeUpdatedAtMs, hotScore };
353
388
  });
354
- if (!hasPinned) return list;
389
+ if (!hasPinned && sortMode !== 'hot') {
390
+ return list;
391
+ }
355
392
  decorated.sort((a, b) => {
356
393
  if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1;
357
394
  if (a.isPinned && a.pinnedAt !== b.pinnedAt) return b.pinnedAt - a.pinnedAt;
395
+ if (sortMode === 'hot') {
396
+ if (a.hotScore !== b.hotScore) return b.hotScore - a.hotScore;
397
+ }
358
398
  return a.index - b.index;
359
399
  });
360
400
  return decorated.map(item => item.session);
@@ -456,6 +496,138 @@ export function createSessionComputed() {
456
496
  range: this.sessionsUsageTimeRange
457
497
  });
458
498
  },
499
+ sessionUsageHeatmap() {
500
+ const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
501
+ ? this.sessionUsageCharts.filteredSessions
502
+ : this.sessionsUsageList;
503
+ const heatmap = buildUsageHeatmap(sessions, { range: this.sessionsUsageTimeRange });
504
+ const t = typeof this.t === 'function' ? this.t : null;
505
+ const lang = typeof this.lang === 'string' ? this.lang.trim().toLowerCase() : '';
506
+ const weekdayAxis = lang === 'en'
507
+ ? ['Mon', '', 'Wed', '', 'Fri', '', '']
508
+ : ['周一', '', '周三', '', '周五', '', ''];
509
+ const months = lang === 'en'
510
+ ? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
511
+ : ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
512
+ const windowWeeks = 52;
513
+ const allWeeks = Array.isArray(heatmap.weeks) ? heatmap.weeks : [];
514
+ const safeWindowWeeks = Math.max(1, Math.min(windowWeeks, allWeeks.length));
515
+ const startIndex = Math.max(0, allWeeks.length - safeWindowWeeks);
516
+ const displayWeeksRaw = allWeeks.slice(startIndex);
517
+ let max = 0;
518
+ for (const week of displayWeeksRaw) {
519
+ const days = Array.isArray(week.days) ? week.days : [];
520
+ for (const cell of days) {
521
+ if (cell && cell.isInRange) {
522
+ max = Math.max(max, cell.sessionCount || 0);
523
+ }
524
+ }
525
+ }
526
+ max = Math.max(1, max);
527
+ const dayMs = 24 * 60 * 60 * 1000;
528
+ let lastMonth = -1;
529
+ const weeks = displayWeeksRaw.map((week) => {
530
+ const idx = Number.isFinite(Number(week.weekIndex)) ? Number(week.weekIndex) : 0;
531
+ const weekStartMs = (Number.isFinite(Number(heatmap.alignedStart)) ? Number(heatmap.alignedStart) : 0) + (idx * 7 * dayMs);
532
+ const month = Number.isFinite(weekStartMs) ? new Date(weekStartMs).getUTCMonth() : -1;
533
+ const monthLabel = (month >= 0 && month <= 11 && month !== lastMonth) ? months[month] : '';
534
+ if (month >= 0 && month <= 11) {
535
+ lastMonth = month;
536
+ }
537
+ return {
538
+ ...week,
539
+ monthLabel,
540
+ days: (Array.isArray(week.days) ? week.days : []).map((cell) => {
541
+ if (!cell) return null;
542
+ if (!cell.isInRange) {
543
+ return {
544
+ ...cell,
545
+ level: -1,
546
+ title: '',
547
+ ariaLabel: ''
548
+ };
549
+ }
550
+ const ratio = cell.sessionCount > 0 ? (cell.sessionCount / max) : 0;
551
+ const level = cell.sessionCount <= 0
552
+ ? 0
553
+ : (ratio <= 0.25 ? 1 : (ratio <= 0.5 ? 2 : (ratio <= 0.75 ? 3 : 4)));
554
+ const tokensTitle = formatUsageSummaryNumber(cell.tokenTotal || 0);
555
+ const title = t
556
+ ? t('usage.heatmap.tooltip', {
557
+ date: cell.dateKey,
558
+ sessions: cell.sessionCount,
559
+ messages: cell.messageCount,
560
+ tokens: tokensTitle
561
+ })
562
+ : `${cell.dateKey} · sessions ${cell.sessionCount} · messages ${cell.messageCount} · tokens ${tokensTitle}`;
563
+ const ariaLabel = t
564
+ ? t('usage.heatmap.aria', {
565
+ date: cell.dateKey,
566
+ sessions: cell.sessionCount
567
+ })
568
+ : `${cell.dateKey} sessions ${cell.sessionCount}`;
569
+ return {
570
+ ...cell,
571
+ level,
572
+ title,
573
+ ariaLabel
574
+ };
575
+ })
576
+ };
577
+ });
578
+ return {
579
+ ...heatmap,
580
+ weeks,
581
+ weekdayAxis
582
+ };
583
+ },
584
+ sessionUsageHourlyHeatmap() {
585
+ const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
586
+ ? this.sessionUsageCharts.filteredSessions
587
+ : this.sessionsUsageList;
588
+ const result = buildUsageHourlyHeatmap(sessions, { range: this.sessionsUsageTimeRange });
589
+ const t = typeof this.t === 'function' ? this.t : null;
590
+ const lang = typeof this.lang === 'string' ? this.lang.trim().toLowerCase() : '';
591
+ const weekdayLabelsZh = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
592
+ const weekdayLabelsEn = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
593
+ const weekdayLabels = lang === 'en' ? weekdayLabelsEn : weekdayLabelsZh;
594
+ const max = Math.max(1, result.maxSessionCount);
595
+ const grid = Array.isArray(result.grid) ? result.grid : [];
596
+ const rows = grid.map((cells, dayIndex) => ({
597
+ weekday: weekdayLabels[dayIndex] || '',
598
+ cells: cells.map((cell, hourIndex) => {
599
+ const ratio = cell.sessionCount > 0 ? (cell.sessionCount / max) : 0;
600
+ const level = cell.sessionCount <= 0
601
+ ? 0
602
+ : (ratio <= 0.25 ? 1 : (ratio <= 0.5 ? 2 : (ratio <= 0.75 ? 3 : 4)));
603
+ const hourLabel = String(hourIndex).padStart(2, '0');
604
+ const tooltipText = t
605
+ ? t('usage.hourlyHeatmap.tooltip', {
606
+ weekday: weekdayLabels[dayIndex],
607
+ hour: hourLabel,
608
+ sessions: cell.sessionCount,
609
+ messages: cell.messageCount,
610
+ tokens: (cell.tokenTotal || 0).toLocaleString('en-US')
611
+ })
612
+ : `${weekdayLabels[dayIndex] || ''} ${hourLabel}:00 · ${cell.sessionCount} sessions · ${cell.messageCount} messages · ${(cell.tokenTotal || 0).toLocaleString('en-US')} tokens`;
613
+ return {
614
+ hour: hourIndex,
615
+ hourLabel,
616
+ sessionCount: cell.sessionCount,
617
+ messageCount: cell.messageCount,
618
+ tokenTotal: cell.tokenTotal,
619
+ level,
620
+ tooltip: tooltipText
621
+ };
622
+ })
623
+ }));
624
+ return {
625
+ range: result.range,
626
+ rows,
627
+ hourLabels: result.hourLabels,
628
+ maxSessionCount: result.maxSessionCount
629
+ };
630
+ },
459
631
  sessionUsageSummaryCards() {
460
632
  const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary
461
633
  ? this.sessionUsageCharts.summary
@@ -570,10 +742,15 @@ export function createSessionComputed() {
570
742
  const baseBuckets = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.buckets)
571
743
  ? this.sessionUsageCharts.buckets
572
744
  : [];
573
- const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
574
- ? this.sessionUsageCharts.filteredSessions
575
- : this.sessionsUsageList;
576
745
  const pricingIndex = buildUsagePricingIndex(this.providersList);
746
+ const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
747
+ const sessions = compareEnabled
748
+ ? this.sessionsUsageList
749
+ : (this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
750
+ ? this.sessionUsageCharts.filteredSessions
751
+ : this.sessionsUsageList);
752
+ const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7;
753
+ const dayMs = 24 * 60 * 60 * 1000;
577
754
  const byDay = new Map();
578
755
 
579
756
  for (const bucket of baseBuckets) {
@@ -588,6 +765,24 @@ export function createSessionComputed() {
588
765
  estimatedSessions: 0,
589
766
  hasCostEstimate: false
590
767
  });
768
+ if (compareEnabled) {
769
+ const baseMs = Date.parse(`${bucket.key}T00:00:00.000Z`);
770
+ if (Number.isFinite(baseMs)) {
771
+ const prevKey = new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10);
772
+ if (!byDay.has(prevKey)) {
773
+ byDay.set(prevKey, {
774
+ key: prevKey,
775
+ label: prevKey.slice(5),
776
+ sessionCount: 0,
777
+ messageCount: 0,
778
+ tokenTotal: 0,
779
+ estimatedCostUsd: 0,
780
+ estimatedSessions: 0,
781
+ hasCostEstimate: false
782
+ });
783
+ }
784
+ }
785
+ }
591
786
  }
592
787
 
593
788
  for (const session of (Array.isArray(sessions) ? sessions : [])) {
@@ -618,20 +813,44 @@ export function createSessionComputed() {
618
813
  }
619
814
  }
620
815
 
621
- // UI 展示:当天在最上面(倒序)。
622
- const rows = [...byDay.values()].sort((a, b) => b.key.localeCompare(a.key, 'en-US'));
623
- const maxTokens = rows.reduce((max, item) => Math.max(max, item.tokenTotal), 0);
624
- const maxCost = rows.reduce((max, item) => Math.max(max, item.estimatedCostUsd), 0);
816
+ const currentKeys = baseBuckets.map((bucket) => bucket && bucket.key).filter(Boolean);
817
+ const rows = currentKeys
818
+ .map((key) => byDay.get(key))
819
+ .filter(Boolean)
820
+ .sort((a, b) => b.key.localeCompare(a.key, 'en-US'));
821
+ const rowsWithCompare = rows.map((row) => {
822
+ if (!compareEnabled) {
823
+ return { ...row, compareEnabled: false, prevKey: '', prevTokenTotal: 0, prevCostUsd: 0 };
824
+ }
825
+ const baseMs = Date.parse(`${row.key}T00:00:00.000Z`);
826
+ const prevKey = Number.isFinite(baseMs)
827
+ ? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
828
+ : '';
829
+ const prevRow = prevKey ? byDay.get(prevKey) : null;
830
+ return {
831
+ ...row,
832
+ compareEnabled: true,
833
+ prevKey,
834
+ prevTokenTotal: prevRow ? prevRow.tokenTotal : 0,
835
+ prevCostUsd: prevRow ? prevRow.estimatedCostUsd : 0
836
+ };
837
+ });
838
+ const maxTokens = rowsWithCompare.reduce((max, item) => Math.max(max, item.tokenTotal, item.prevTokenTotal || 0), 0);
839
+ const maxCost = rowsWithCompare.reduce((max, item) => Math.max(max, item.estimatedCostUsd, item.prevCostUsd || 0), 0);
625
840
 
626
841
  return {
627
- rows: rows.map((row) => ({
842
+ rows: rowsWithCompare.map((row) => ({
628
843
  ...row,
629
844
  tokenLabel: formatCompactUsageSummaryNumber(row.tokenTotal),
630
845
  tokenTitle: formatUsageSummaryNumber(row.tokenTotal),
631
846
  tokenPercent: maxTokens > 0 ? Math.round((row.tokenTotal / maxTokens) * 1000) / 10 : 0,
847
+ prevTokenPercent: row.compareEnabled && maxTokens > 0 ? Math.round(((row.prevTokenTotal || 0) / maxTokens) * 1000) / 10 : 0,
848
+ prevTokenTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',
632
849
  costLabel: row.hasCostEstimate ? formatUsageEstimatedCost(row.estimatedCostUsd) : '0',
633
850
  costTitle: row.hasCostEstimate ? formatUsageEstimatedCost(row.estimatedCostUsd, { precise: true }) : '0',
634
- costPercent: maxCost > 0 ? Math.round((row.estimatedCostUsd / maxCost) * 1000) / 10 : 0
851
+ costPercent: maxCost > 0 ? Math.round((row.estimatedCostUsd / maxCost) * 1000) / 10 : 0,
852
+ prevCostPercent: row.compareEnabled && maxCost > 0 ? Math.round(((row.prevCostUsd || 0) / maxCost) * 1000) / 10 : 0,
853
+ prevCostTitle: row.compareEnabled ? formatUsageEstimatedCost(row.prevCostUsd || 0, { precise: true }) : ''
635
854
  })),
636
855
  maxTokens,
637
856
  maxCost
@@ -645,6 +864,111 @@ export function createSessionComputed() {
645
864
  return daily && Array.isArray(daily.rows) ? daily.rows : [];
646
865
  },
647
866
 
867
+ sessionsUsageSelectedDaySummary() {
868
+ const dayKey = typeof this.sessionsUsageSelectedDayKey === 'string' ? this.sessionsUsageSelectedDayKey.trim() : '';
869
+ if (!dayKey) return null;
870
+ const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
871
+ ? this.sessionUsageCharts.filteredSessions
872
+ : this.sessionsUsageList;
873
+ const pricingIndex = buildUsagePricingIndex(this.providersList);
874
+ const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all';
875
+ const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7;
876
+ const dayMs = 24 * 60 * 60 * 1000;
877
+ const baseMs = Date.parse(`${dayKey}T00:00:00.000Z`);
878
+ const prevKey = compareEnabled && Number.isFinite(baseMs)
879
+ ? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
880
+ : '';
881
+ let sessionCount = 0;
882
+ let messageCount = 0;
883
+ let tokenTotal = 0;
884
+ let estimatedCostUsd = 0;
885
+ let hasCostEstimate = false;
886
+ let prevTokenTotal = 0;
887
+ let prevEstimatedCostUsd = 0;
888
+ let prevHasCostEstimate = false;
889
+ const modelMap = new Map();
890
+ const sessionRows = [];
891
+ for (const session of (Array.isArray(sessions) ? sessions : [])) {
892
+ if (!session || typeof session !== 'object') continue;
893
+ const updatedAtMs = Date.parse(session.updatedAt || '');
894
+ if (!Number.isFinite(updatedAtMs)) continue;
895
+ const key = new Date(updatedAtMs).toISOString().slice(0, 10);
896
+ const isCurrent = key === dayKey;
897
+ const isPrev = !!prevKey && key === prevKey;
898
+ if (!isCurrent && !isPrev) continue;
899
+ const msgCount = Number.isFinite(Number(session.messageCount))
900
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
901
+ : 0;
902
+ const sessionTokens = Number.isFinite(Number(session.totalTokens))
903
+ ? Math.max(0, Math.floor(Number(session.totalTokens)))
904
+ : 0;
905
+ if (isCurrent) {
906
+ sessionCount += 1;
907
+ messageCount += msgCount;
908
+ tokenTotal += sessionTokens;
909
+ } else if (isPrev) {
910
+ prevTokenTotal += sessionTokens;
911
+ }
912
+ if (shouldEstimateUsageCostForSession(session)) {
913
+ const cost = estimateUsageCostForSession(session, pricingIndex, this.currentProvider);
914
+ if (cost.pricing && cost.hasTokenBreakdown) {
915
+ if (isCurrent) {
916
+ estimatedCostUsd += cost.estimatedUsd;
917
+ hasCostEstimate = true;
918
+ } else if (isPrev) {
919
+ prevEstimatedCostUsd += cost.estimatedUsd;
920
+ prevHasCostEstimate = true;
921
+ }
922
+ }
923
+ }
924
+ const model = typeof session.model === 'string' ? session.model.trim() : '';
925
+ if (isCurrent && model) {
926
+ modelMap.set(model, (modelMap.get(model) || 0) + 1);
927
+ }
928
+ const title = typeof session.title === 'string' && session.title.trim()
929
+ ? session.title.trim()
930
+ : (typeof session.sessionId === 'string' && session.sessionId.trim() ? session.sessionId.trim() : '未命名会话');
931
+ if (isCurrent) {
932
+ sessionRows.push({
933
+ key: this.getSessionExportKey(session) || `${title}:${sessionCount}`,
934
+ title,
935
+ messageCount: msgCount
936
+ });
937
+ }
938
+ }
939
+ sessionRows.sort((a, b) => b.messageCount - a.messageCount);
940
+ const lang = typeof this.lang === 'string' ? this.lang.trim().toLowerCase() : '';
941
+ const suffix = lang === 'en' ? 'msgs' : '条';
942
+ const topSessions = sessionRows.slice(0, 8).map((item) => ({
943
+ ...item,
944
+ messageCountLabel: `${item.messageCount} ${suffix}`
945
+ }));
946
+ const topModels = [...modelMap.entries()]
947
+ .sort((a, b) => b[1] - a[1])
948
+ .slice(0, 6)
949
+ .map(([model, count]) => ({
950
+ key: model,
951
+ label: `${model} · ${count}`
952
+ }));
953
+ return {
954
+ dayKey,
955
+ compareEnabled,
956
+ prevKey,
957
+ sessionCount,
958
+ messageCount,
959
+ tokenTotal,
960
+ tokenLabel: formatUsageSummaryNumber(tokenTotal),
961
+ costLabel: hasCostEstimate ? formatUsageEstimatedCost(estimatedCostUsd) : '0',
962
+ prevTokenTotal,
963
+ prevTokenLabel: compareEnabled ? formatUsageSummaryNumber(prevTokenTotal) : '0',
964
+ deltaTokenLabel: compareEnabled ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : '0',
965
+ prevCostLabel: compareEnabled ? (prevHasCostEstimate ? formatUsageEstimatedCost(prevEstimatedCostUsd) : '0') : '0',
966
+ deltaCostLabel: compareEnabled ? formatSignedUsageEstimatedCost(estimatedCostUsd - prevEstimatedCostUsd, { precise: true }) : '0',
967
+ topSessions,
968
+ topModels
969
+ };
970
+ },
971
+
648
972
  visibleSessionTrashItems() {
649
973
  const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : [];
650
974
  const visibleCount = Number(this.sessionTrashVisibleCount);
@@ -4,6 +4,7 @@ export function createClaudeConfigMethods(options = {}) {
4
4
  return {
5
5
  switchClaudeConfig(name) {
6
6
  this.currentClaudeConfig = name;
7
+ try { localStorage.setItem('currentClaudeConfig', name || ''); } catch (_) {}
7
8
  this.refreshClaudeModelContext();
8
9
  },
9
10
 
@@ -20,6 +21,7 @@ export function createClaudeConfigMethods(options = {}) {
20
21
  }
21
22
  const existing = this.claudeConfigs[name] || {};
22
23
  this.currentClaudeModel = model;
24
+ this.claudeCustomModelDraft = model;
23
25
  this.claudeConfigs[name] = this.mergeClaudeConfig(existing, { model });
24
26
  this.saveClaudeConfigs();
25
27
  this.updateClaudeModelsCurrent();
@@ -30,8 +32,15 @@ export function createClaudeConfigMethods(options = {}) {
30
32
  this.applyClaudeConfig(name);
31
33
  },
32
34
 
35
+ onClaudeCustomModelSubmit() {
36
+ this.onClaudeModelChange();
37
+ },
38
+
33
39
  saveClaudeConfigs() {
34
- localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
40
+ try { localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs)); } catch (_) {}
41
+ if (this.currentClaudeConfig) {
42
+ try { localStorage.setItem('currentClaudeConfig', this.currentClaudeConfig); } catch (_) {}
43
+ }
35
44
  },
36
45
 
37
46
  openEditConfigModal(name) {
@@ -138,6 +147,7 @@ export function createClaudeConfigMethods(options = {}) {
138
147
 
139
148
  async applyClaudeConfig(name) {
140
149
  this.currentClaudeConfig = name;
150
+ try { localStorage.setItem('currentClaudeConfig', name || ''); } catch (_) {}
141
151
  this.refreshClaudeModelContext();
142
152
  const config = this.claudeConfigs[name];
143
153
 
@@ -1,5 +1,10 @@
1
1
  import { runLatestOnlyQueue } from '../logic.mjs';
2
2
  import { normalizeConfigTemplateDiffConfirmEnabled } from './config-template-confirm-pref.mjs';
3
+ import {
4
+ getConvertTargetSource,
5
+ normalizeSessionConvertSource
6
+ } from '../logic.session-convert.mjs';
7
+ import { syncSessionsFilterUrl } from './sessions-filters-url.mjs';
3
8
 
4
9
  function hasResponseError(response) {
5
10
  if (!response || typeof response !== 'object') {
@@ -75,6 +80,77 @@ export function createCodexConfigMethods(options = {}) {
75
80
  }
76
81
  },
77
82
 
83
+ async convertSession(session) {
84
+ const source = normalizeSessionConvertSource(session && session.source ? session.source : '');
85
+ const target = getConvertTargetSource(source);
86
+ if (!source || !target) {
87
+ this.showMessage('不支持此操作', 'error');
88
+ return;
89
+ }
90
+ const key = this.getSessionExportKey(session);
91
+ if (this.sessionConverting[key]) return;
92
+ this.sessionConverting[key] = true;
93
+ try {
94
+ const res = await api('convert-session', {
95
+ source,
96
+ target,
97
+ sessionId: session.sessionId,
98
+ filePath: session.filePath,
99
+ maxMessages: 'all'
100
+ });
101
+ if (res && res.error) {
102
+ this.showMessage(res.error, 'error');
103
+ return;
104
+ }
105
+ const converted = res && res.session ? res.session : null;
106
+ if (!converted) {
107
+ this.showMessage('转换失败', 'error');
108
+ return;
109
+ }
110
+ if (res && res.truncated) {
111
+ const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
112
+ const targetLabel = converted && converted.sourceLabel ? String(converted.sourceLabel).trim() : '';
113
+ if (targetLabel && typeof this.sessionFilterSource === 'string' && this.sessionFilterSource !== converted.source) {
114
+ this.showMessage(`已生成派生会话(来源:${targetLabel},已截断:最多 ${maxLabel} 条消息)`, 'info');
115
+ } else {
116
+ this.showMessage(`已生成派生会话(已截断:最多 ${maxLabel} 条消息)`, 'info');
117
+ }
118
+ } else {
119
+ const targetLabel = converted && converted.sourceLabel ? String(converted.sourceLabel).trim() : '';
120
+ if (targetLabel && typeof this.sessionFilterSource === 'string' && this.sessionFilterSource !== converted.source) {
121
+ this.showMessage(`已生成派生会话(来源:${targetLabel})`, 'success');
122
+ } else {
123
+ this.showMessage('已生成派生会话', 'success');
124
+ }
125
+ }
126
+
127
+ if (converted && converted.source && typeof this.sessionFilterSource === 'string') {
128
+ if (this.sessionFilterSource !== converted.source) {
129
+ this.sessionFilterSource = converted.source;
130
+ if (typeof this.refreshSessionPathOptions === 'function') {
131
+ this.refreshSessionPathOptions(this.sessionFilterSource);
132
+ }
133
+ if (typeof this.persistSessionFilterCache === 'function') {
134
+ this.persistSessionFilterCache();
135
+ }
136
+ syncSessionsFilterUrl(this);
137
+ this.sessionsList = [converted];
138
+ } else {
139
+ const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
140
+ const next = [converted, ...list.filter(item => !(item && item.filePath === converted.filePath && item.source === converted.source))];
141
+ this.sessionsList = next;
142
+ }
143
+ }
144
+ if (typeof this.selectSession === 'function') {
145
+ await this.selectSession(converted);
146
+ }
147
+ } catch (e) {
148
+ this.showMessage('转换失败', 'error');
149
+ } finally {
150
+ this.sessionConverting[key] = false;
151
+ }
152
+ },
153
+
78
154
  async quickSwitchProvider(name) {
79
155
  const target = String(name || '').trim();
80
156
  const visualTarget = String(this.providerSwitchDisplayTarget || '').trim();
@@ -55,11 +55,18 @@ export function createNavigationMethods(options = {}) {
55
55
  return null;
56
56
  }
57
57
  };
58
- const persistNavState = (vm) => {
58
+ const persistNavState = (vm, overrides = null) => {
59
59
  if (!vm || vm.__navStateRestoring) return;
60
60
  if (typeof localStorage === 'undefined') return;
61
- const mainTab = typeof vm.mainTab === 'string' ? vm.mainTab.trim().toLowerCase() : '';
62
- const configMode = typeof vm.configMode === 'string' ? vm.configMode.trim().toLowerCase() : '';
61
+ const resolvedOverrides = overrides && typeof overrides === 'object' ? overrides : null;
62
+ const mainTabSource = resolvedOverrides && typeof resolvedOverrides.mainTab === 'string'
63
+ ? resolvedOverrides.mainTab
64
+ : vm.mainTab;
65
+ const configModeSource = resolvedOverrides && typeof resolvedOverrides.configMode === 'string'
66
+ ? resolvedOverrides.configMode
67
+ : vm.configMode;
68
+ const mainTab = typeof mainTabSource === 'string' ? mainTabSource.trim().toLowerCase() : '';
69
+ const configMode = typeof configModeSource === 'string' ? configModeSource.trim().toLowerCase() : '';
63
70
  const snapshot = {
64
71
  mainTab: MAIN_TAB_SET.has(mainTab) ? mainTab : 'dashboard',
65
72
  configMode: configModeSet && configModeSet.has(configMode) ? configMode : 'codex'
@@ -70,6 +77,34 @@ export function createNavigationMethods(options = {}) {
70
77
  };
71
78
 
72
79
  return {
80
+ restoreNavStateFromStorage() {
81
+ if (this.__navStateRestoring) return false;
82
+ const restored = readNavState();
83
+ if (!restored) return false;
84
+ const nextMainTab = restored && typeof restored.mainTab === 'string'
85
+ ? restored.mainTab.trim().toLowerCase()
86
+ : '';
87
+ const nextConfigMode = restored && typeof restored.configMode === 'string'
88
+ ? restored.configMode.trim().toLowerCase()
89
+ : '';
90
+ const shouldUpdateConfigMode = !!(nextConfigMode && configModeSet && configModeSet.has(nextConfigMode));
91
+ const shouldUpdateMainTab = !!(nextMainTab && MAIN_TAB_SET.has(nextMainTab) && nextMainTab !== this.mainTab);
92
+ if (!shouldUpdateConfigMode && !shouldUpdateMainTab) {
93
+ return false;
94
+ }
95
+ this.__navStateRestoring = true;
96
+ try {
97
+ if (shouldUpdateConfigMode) {
98
+ this.configMode = nextConfigMode;
99
+ }
100
+ if (shouldUpdateMainTab) {
101
+ this.switchMainTab(nextMainTab);
102
+ }
103
+ } finally {
104
+ this.__navStateRestoring = false;
105
+ }
106
+ return true;
107
+ },
73
108
  switchConfigMode(mode) {
74
109
  const normalizedMode = typeof mode === 'string'
75
110
  ? mode.trim().toLowerCase()
@@ -101,6 +136,10 @@ export function createNavigationMethods(options = {}) {
101
136
  persistNavState(this);
102
137
  return;
103
138
  }
139
+ persistNavState(this, {
140
+ mainTab: 'config',
141
+ configMode: normalizedMode
142
+ });
104
143
  this.switchMainTab('config');
105
144
  },
106
145
 
@@ -254,6 +293,7 @@ export function createNavigationMethods(options = {}) {
254
293
  }
255
294
  const normalizedTab = typeof tab === 'string' ? tab.trim().toLowerCase() : '';
256
295
  if (!normalizedTab) return;
296
+ persistNavState(this, { mainTab: normalizedTab });
257
297
  this.setMainTabSwitchIntent(normalizedTab);
258
298
  this.applyImmediateNavIntent(normalizedTab);
259
299
  const shouldHideSessionPanel = this.mainTab === 'sessions' && normalizedTab !== 'sessions';
@@ -275,6 +315,7 @@ export function createNavigationMethods(options = {}) {
275
315
  }
276
316
  const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
277
317
  if (!normalizedMode) return;
318
+ persistNavState(this, { mainTab: 'config', configMode: normalizedMode });
278
319
  this.setMainTabSwitchIntent('config');
279
320
  if (typeof this.ensureMainTabSwitchState === 'function') {
280
321
  this.ensureMainTabSwitchState().pendingConfigMode = normalizedMode;
@@ -345,6 +386,10 @@ export function createNavigationMethods(options = {}) {
345
386
  if (targetTab === 'orchestration' && this.taskOrchestrationTabEnabled !== true) {
346
387
  return this.switchMainTab('config');
347
388
  }
389
+ persistNavState(this, {
390
+ mainTab: targetTab,
391
+ configMode: targetTab === 'config' ? this.configMode : this.configMode
392
+ });
348
393
  this.cancelTouchNavIntentReset();
349
394
  if (targetTab === 'sessions') {
350
395
  this.cancelScheduledSessionTabDeferredTeardown();
@@ -583,6 +628,9 @@ export function createNavigationMethods(options = {}) {
583
628
  this.__sessionListRef = nextRef;
584
629
  }
585
630
  this.scheduleSessionListViewportFill();
631
+ if (typeof this.scheduleSessionListMessageCountHydrate === 'function') {
632
+ this.scheduleSessionListMessageCountHydrate();
633
+ }
586
634
  },
587
635
 
588
636
  resetSessionPreviewMessageRender() {