Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@
"analytics_top_sessions_by_messages": "By Messages",
"analytics_top_sessions_by_duration": "By Duration",
"analytics_top_sessions_by_output_tokens": "By Output Tokens",
"analytics_top_sessions_active_duration": "Active duration",
"analytics_top_sessions_total_duration": "{duration} total",
"analytics_projects_title": "Projects",
"analytics_project_messages": [
{
Expand Down
2 changes: 2 additions & 0 deletions frontend/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@
"analytics_top_sessions_by_messages": "按消息数",
"analytics_top_sessions_by_duration": "按时长",
"analytics_top_sessions_by_output_tokens": "按输出 token",
"analytics_top_sessions_active_duration": "活跃时长",
"analytics_top_sessions_total_duration": "总计 {duration}",
"analytics_projects_title": "项目",
"analytics_project_messages": [
{
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/api/generated/models/DbTopSession.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/src/lib/api/types/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export interface TopSession {
message_count: number;
output_tokens: number;
duration_min: number;
active_duration_min: number;
/** ISO timestamps used by the StatusDot component to compute
* the active/stale/unclean tier — the column needs the same
* recency inputs as the sidebar list. */
Expand Down
25 changes: 23 additions & 2 deletions frontend/src/lib/components/analytics/TopSessions.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@
return m > 0 ? `${h}h ${m}m` : `${h}h`;
}

function formatDurationWithTotal(
activeMin: number,
totalMin: number,
): string {
return `${formatDuration(activeMin)} (${m.analytics_top_sessions_total_duration({
duration: formatDuration(totalMin),
})})`;
}

function handleSessionClick(id: string) {
let needInvalidate = false;
const params: Record<string, string> = {};
Expand Down Expand Up @@ -154,7 +163,15 @@
</div>
<span class="session-metric">
{#if analytics.topMetric === "duration"}
{formatDuration(session.duration_min)}
<span
class="session-metric-primary"
title={m.analytics_top_sessions_active_duration()}
>
{formatDurationWithTotal(
session.active_duration_min,
session.duration_min,
)}
</span>
{:else if analytics.topMetric === "output_tokens"}
{formatTokenCount(session.output_tokens)}
{:else}
Expand Down Expand Up @@ -322,10 +339,14 @@
font-weight: 500;
font-family: var(--font-mono);
color: var(--accent-blue);
min-width: 36px;
min-width: 86px;
text-align: right;
}

.session-metric-primary {
display: inline-block;
}

.empty {
color: var(--text-muted);
font-size: 12px;
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/lib/components/analytics/TopSessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ describe("TopSessions", () => {
message_count: 10,
output_tokens: 0,
duration_min: 5,
active_duration_min: 5,
},
],
};
Expand Down Expand Up @@ -184,6 +185,7 @@ describe("TopSessions", () => {
message_count: 10,
output_tokens: 0,
duration_min: 5,
active_duration_min: 5,
},
],
};
Expand All @@ -208,6 +210,44 @@ describe("TopSessions", () => {
unmount(component);
});

it("shows active duration as primary with total duration context", async () => {
// @ts-ignore — topMetric is a reactive store field
analytics.topMetric = "duration";
analytics.topSessions = {
metric: "duration",
sessions: [
{
id: "sess-1",
project: "proj",
first_message: "hello",
message_count: 10,
output_tokens: 0,
duration_min: 120,
active_duration_min: 2.5,
},
],
};
// @ts-ignore — loading is reactive state
analytics.loading = {
...analytics.loading,
topSessions: false,
};
// @ts-ignore
analytics.errors = {
...analytics.errors,
topSessions: null,
};

const component = mount(TopSessions, { target: document.body });
await tick();

expect(
document.querySelector(".session-metric")?.textContent?.trim(),
).toBe("3m (2h total)");

unmount(component);
});

describe("status column", () => {
function mountWithFourStates() {
analytics.topSessions = {
Expand All @@ -220,6 +260,7 @@ describe("TopSessions", () => {
message_count: 1,
output_tokens: 0,
duration_min: 0,
active_duration_min: 0,
termination_status: "clean",
},
{
Expand All @@ -229,6 +270,7 @@ describe("TopSessions", () => {
message_count: 1,
output_tokens: 0,
duration_min: 0,
active_duration_min: 0,
termination_status: "tool_call_pending",
},
{
Expand All @@ -238,6 +280,7 @@ describe("TopSessions", () => {
message_count: 1,
output_tokens: 0,
duration_min: 0,
active_duration_min: 0,
termination_status: "truncated",
},
{
Expand All @@ -247,6 +290,7 @@ describe("TopSessions", () => {
message_count: 1,
output_tokens: 0,
duration_min: 0,
active_duration_min: 0,
termination_status: null,
},
],
Expand Down Expand Up @@ -323,6 +367,7 @@ describe("TopSessions", () => {
message_count: 1,
output_tokens: 0,
duration_min: 0,
active_duration_min: 0,
termination_status: "clean",
},
],
Expand Down
Loading