diff --git a/cmd/agentsview/activity.go b/cmd/agentsview/activity.go index 3bcb40679..1987cd7eb 100644 --- a/cmd/agentsview/activity.go +++ b/cmd/agentsview/activity.go @@ -17,7 +17,6 @@ import ( "go.kenn.io/agentsview/internal/db" ) -// ActivityReportConfig holds the flags for `agentsview activity report`. type ActivityReportConfig struct { Preset string Date string @@ -33,7 +32,6 @@ type ActivityReportConfig struct { Offline bool } -// runActivityReport syncs, resolves the range, runs the report, and prints it. func runActivityReport(cfg ActivityReportConfig) { ctx := context.Background() backend, cleanup, err := resolveArchiveQueryBackend(ctx, archiveQueryPolicy{ @@ -180,9 +178,6 @@ func todayIn(tz string) string { return time.Now().In(loc).Format("2006-01-02") } -// printActivityReport renders the human-readable report: a header, totals, -// peak concurrency, top breakdowns, and top sessions. It deliberately omits -// the dense per-bucket timeline, which only the --json output exposes. func printActivityReport(r activity.Report) { loc, err := time.LoadLocation(r.Timezone) if err != nil { @@ -205,10 +200,10 @@ func printActivityReport(r activity.Report) { printKeyMinutes("By project", r.ByProject) printKeyMinutes("By model", r.ByModel) printKeyMinutes("By agent", r.ByAgent) + printBranchKeyMinutes("By branch", r.ByBranch) printActivitySessions(r.BySession) } -// printActivityTotals prints the totals block via a tabwriter. func printActivityTotals(t activity.Totals) { w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) fmt.Fprintf(w, "Active minutes\t%.1f\n", t.ActiveMinutes) @@ -222,14 +217,24 @@ func printActivityTotals(t activity.Totals) { w.Flush() } -// printActivityPeak prints peak concurrency and when it occurred, in loc. func printActivityPeak(p activity.Peak, loc *time.Location) { fmt.Printf("Peak concurrency: %d agents at %s\n", p.Agents, fmtInstant(p.At, loc)) } -// printKeyMinutes prints the top 5 rows of a key/agent-minutes breakdown. func printKeyMinutes(label string, rows []activity.KeyMinutes) { + printLabeledKeyMinutes(label, rows, nil) +} + +func printBranchKeyMinutes(label string, rows []activity.KeyMinutes) { + printLabeledKeyMinutes(label, rows, activity.BranchKeyLabel) +} + +func printLabeledKeyMinutes( + label string, + rows []activity.KeyMinutes, + labelKey func(string) string, +) { fmt.Printf("%s (top 5):\n", label) if len(rows) == 0 { fmt.Println(" (none)") @@ -238,14 +243,17 @@ func printKeyMinutes(label string, rows []activity.KeyMinutes) { } w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) for _, row := range topKeyMinutes(rows, 5) { + key := row.Key + if labelKey != nil { + key = labelKey(key) + } fmt.Fprintf(w, " %s\t%.1f min\n", - sanitizeTerminal(row.Key), row.AgentMinutes) + sanitizeTerminal(key), row.AgentMinutes) } w.Flush() fmt.Println() } -// printActivitySessions prints the top 5 sessions by appearance order. func printActivitySessions(rows []activity.SessionRow) { fmt.Println("Top sessions (top 5):") if len(rows) == 0 { @@ -265,7 +273,6 @@ func printActivitySessions(rows []activity.SessionRow) { w.Flush() } -// topKeyMinutes returns the first n rows of rows (already sorted by the query). func topKeyMinutes(rows []activity.KeyMinutes, n int) []activity.KeyMinutes { return rows[:min(len(rows), n)] } @@ -284,8 +291,6 @@ func fmtRangeBound(ts string, loc *time.Location) string { return t.Format("2006-01-02 15:04") } -// fmtMinutes renders an agent-minutes value, printing a dash for untimed -// sessions whose pointer is nil. func fmtMinutes(m *float64) string { if m == nil { return "—" diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 1af05cef1..0502eabda 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -778,6 +778,7 @@ "activity_project": "Project", "activity_model": "Model", "activity_agent": "Agent", + "activity_branch": "Branch", "activity_min_unit": " min", "activity_int_auto_split": "int {int} / auto {auto}", "activity_breakdown": "Breakdown", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index 9bad54a70..0362577bf 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -757,6 +757,7 @@ "activity_project": "项目", "activity_model": "模型", "activity_agent": "代理", + "activity_branch": "分支", "activity_min_unit": " 分钟", "activity_int_auto_split": "交互 {int} / 自动 {auto}", "activity_breakdown": "细分", diff --git a/frontend/src/lib/api/generated/models/ActivityReport.ts b/frontend/src/lib/api/generated/models/ActivityReport.ts index fd1923026..fb3c55804 100644 --- a/frontend/src/lib/api/generated/models/ActivityReport.ts +++ b/frontend/src/lib/api/generated/models/ActivityReport.ts @@ -11,6 +11,7 @@ export type ActivityReport = { bucket_unit: string; buckets: any[] | null; by_agent: any[] | null; + by_branch: any[] | null; by_model: any[] | null; by_project: any[] | null; by_session: any[] | null; diff --git a/frontend/src/lib/api/types/activity.ts b/frontend/src/lib/api/types/activity.ts index 94e34597e..e58975775 100644 --- a/frontend/src/lib/api/types/activity.ts +++ b/frontend/src/lib/api/types/activity.ts @@ -6,8 +6,55 @@ import type { ActivityKeyMinutes, } from "../generated/index"; -export type Report = ActivityReport; export type Bucket = ActivityBucket; export type ReportInterval = ActivityReportInterval; -export type SessionRow = ActivitySessionRow; +export type SessionRow = Omit & { + models: string[] | null; +}; export type KeyMinutes = ActivityKeyMinutes; + +export type Report = Omit< + ActivityReport, + | "buckets" + | "by_agent" + | "by_branch" + | "by_model" + | "by_project" + | "by_session" + | "intervals" +> & { + buckets: Bucket[] | null; + by_agent: KeyMinutes[] | null; + by_branch: KeyMinutes[] | null; + by_model: KeyMinutes[] | null; + by_project: KeyMinutes[] | null; + by_session: SessionRow[] | null; + intervals: ReportInterval[] | null; +}; + +function generatedRows(rows: unknown): T[] | null { + if (!Array.isArray(rows)) return null; + return rows as T[]; +} + +function sessionRowsFromGenerated(rows: unknown): SessionRow[] | null { + const sessions = generatedRows(rows); + if (sessions === null) return null; + return sessions.map((session) => ({ + ...session, + models: generatedRows(session.models), + })); +} + +export function activityReportFromGenerated(report: ActivityReport): Report { + return { + ...report, + buckets: generatedRows(report.buckets), + by_agent: generatedRows(report.by_agent), + by_branch: generatedRows(report.by_branch), + by_model: generatedRows(report.by_model), + by_project: generatedRows(report.by_project), + by_session: sessionRowsFromGenerated(report.by_session), + intervals: generatedRows(report.intervals), + }; +} diff --git a/frontend/src/lib/components/activity/Breakdowns.svelte b/frontend/src/lib/components/activity/Breakdowns.svelte index 104c961d6..90f31a616 100644 --- a/frontend/src/lib/components/activity/Breakdowns.svelte +++ b/frontend/src/lib/components/activity/Breakdowns.svelte @@ -1,41 +1,29 @@