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.
- package/README.md +11 -3
- package/README.zh.md +10 -2
- package/cli/builtin-proxy.js +315 -95
- package/cli/openai-bridge.js +99 -5
- package/cli/session-convert-args.js +65 -0
- package/cli/session-convert-io.js +82 -0
- package/cli/session-convert.js +43 -0
- package/cli.js +547 -32
- package/package.json +74 -74
- package/web-ui/app.js +24 -2
- package/web-ui/logic.session-convert.mjs +70 -0
- package/web-ui/logic.sessions.mjs +151 -0
- package/web-ui/modules/app.computed.dashboard.mjs +44 -1
- package/web-ui/modules/app.computed.session.mjs +336 -12
- package/web-ui/modules/app.methods.claude-config.mjs +11 -1
- package/web-ui/modules/app.methods.codex-config.mjs +76 -0
- package/web-ui/modules/app.methods.navigation.mjs +51 -3
- package/web-ui/modules/app.methods.session-actions.mjs +55 -3
- package/web-ui/modules/app.methods.session-browser.mjs +270 -3
- package/web-ui/modules/app.methods.session-timeline.mjs +34 -3
- package/web-ui/modules/app.methods.session-trash.mjs +16 -1
- package/web-ui/modules/app.methods.startup-claude.mjs +234 -125
- package/web-ui/modules/i18n.dict.mjs +76 -0
- package/web-ui/partials/index/panel-config-claude.html +12 -4
- package/web-ui/partials/index/panel-sessions.html +33 -10
- package/web-ui/partials/index/panel-settings.html +16 -0
- package/web-ui/partials/index/panel-usage.html +95 -85
- package/web-ui/session-helpers.mjs +3 -0
- package/web-ui/styles/base-theme.css +29 -25
- package/web-ui/styles/layout-shell.css +1 -1
- package/web-ui/styles/navigation-panels.css +9 -9
- package/web-ui/styles/sessions-list.css +17 -0
- package/web-ui/styles/sessions-toolbar-trash.css +62 -0
- package/web-ui/styles/sessions-usage.css +211 -83
- 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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
622
|
-
const rows =
|
|
623
|
-
|
|
624
|
-
|
|
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:
|
|
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
|
|
62
|
-
const
|
|
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() {
|