Skip to content
Closed
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
32 changes: 27 additions & 5 deletions cmd/agentsview/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ 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)
}

Expand Down Expand Up @@ -237,14 +238,37 @@ func printKeyMinutes(label string, rows []activity.KeyMinutes) {
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
for _, row := range topKeyMinutes(rows, 5) {
for _, row := range firstN(rows, 5) {
fmt.Fprintf(w, " %s\t%.1f min\n",
sanitizeTerminal(row.Key), row.AgentMinutes)
}
w.Flush()
fmt.Println()
}

func printBranchKeyMinutes(label string, rows []activity.BranchKeyMinutes) {
fmt.Printf("%s (top 5):\n", label)
if len(rows) == 0 {
fmt.Println(" (none)")
fmt.Println()
return
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
for _, row := range firstN(rows, 5) {
branch := row.Branch
if branch == "" {
branch = "(no branch)"
}
key := branch
if row.Project != "" {
key = row.Project + "/" + branch
}
fmt.Fprintf(w, " %s\t%.1f min\n", 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):")
Expand All @@ -254,8 +278,7 @@ func printActivitySessions(rows []activity.SessionRow) {
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, " TITLE\tPROJECT\tAGENT\tMINUTES\tCOST")
limit := min(len(rows), 5)
for _, s := range rows[:limit] {
for _, s := range firstN(rows, 5) {
fmt.Fprintf(w, " %s\t%s\t%s\t%s\t%s\n",
sanitizeTerminal(s.Title), sanitizeTerminal(s.Project),
sanitizeTerminal(s.Agent),
Expand All @@ -265,8 +288,7 @@ 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 {
func firstN[T any](rows []T, n int) []T {
return rows[:min(len(rows), n)]
}

Expand Down
5 changes: 4 additions & 1 deletion docs/session-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,7 @@ Response excerpt:
"by_project": [{"key": "agentsview", "agent_minutes": 96.4, "cost": 4.20}],
"by_model": [{"key": "claude-sonnet-4-6", "agent_minutes": 80.0, "cost": 3.10}],
"by_agent": [{"key": "codex", "agent_minutes": 64.0, "cost": 2.85}],
"by_branch": [{"project": "agentsview", "branch": "main", "agent_minutes": 96.4, "cost": 4.20}],
"by_session": [
{
"session_id": "codex:abc",
Expand Down Expand Up @@ -768,7 +769,9 @@ Response excerpt:
```

Breakdown rows include total, automated, and interactive minutes and
costs. Session rows with no reliable timestamped activity use
costs. `by_branch` rows carry `project`/`branch` as separate fields; an
empty `branch` means no recorded branch. Session rows with no reliable
timestamped activity use
`"timing_quality": "untimed"` and `agent_minutes: null`; they can
still contribute cost and output tokens when usage rows exist.

Expand Down
5 changes: 5 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,8 @@
"activity_all_agents": "All Agents",
"activity_filter_by_machine": "Filter by machine",
"activity_all_machines": "All Machines",
"activity_filter_by_branch": "Filter by branch",
"activity_all_branches": "All Branches",
"activity_filter_by_automation": "Filter by automation",
"activity_all_sessions": "All Sessions",
"activity_interactive": "Interactive",
Expand All @@ -813,6 +815,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",
Expand Down Expand Up @@ -1660,9 +1663,11 @@
"activity_insight_no_matching_agents": "No matching agents",
"activity_filter_agents_placeholder": "Filter agents",
"activity_filter_machines_placeholder": "Filter machines",
"activity_filter_branches_placeholder": "Filter branches",
"activity_filter_automation_placeholder": "Filter automation",
"activity_no_matching_agents": "No matching agents",
"activity_no_matching_machines": "No matching machines",
"activity_no_matching_branches": "No matching branches",
"activity_no_automation_filters": "No automation filters",
"activity_loading_report": "Loading activity report...",
"appearance_high_contrast": "High contrast",
Expand Down
5 changes: 5 additions & 0 deletions frontend/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,8 @@
"activity_all_agents": "全部代理",
"activity_filter_by_machine": "按机器筛选",
"activity_all_machines": "全部机器",
"activity_filter_by_branch": "按分支筛选",
"activity_all_branches": "全部分支",
"activity_filter_by_automation": "按自动化筛选",
"activity_all_sessions": "全部会话",
"activity_interactive": "交互式",
Expand All @@ -788,6 +790,7 @@
"activity_project": "项目",
"activity_model": "模型",
"activity_agent": "代理",
"activity_branch": "分支",
"activity_min_unit": " 分钟",
"activity_int_auto_split": "交互 {int} / 自动 {auto}",
"activity_breakdown": "细分",
Expand Down Expand Up @@ -1626,9 +1629,11 @@
"activity_insight_no_matching_agents": "无匹配代理",
"activity_filter_agents_placeholder": "筛选代理",
"activity_filter_machines_placeholder": "筛选机器",
"activity_filter_branches_placeholder": "筛选分支",
"activity_filter_automation_placeholder": "筛选自动化",
"activity_no_matching_agents": "无匹配代理",
"activity_no_matching_machines": "无匹配机器",
"activity_no_matching_branches": "无匹配分支",
"activity_no_automation_filters": "无自动化筛选",
"activity_loading_report": "正在加载活动报告...",
"appearance_high_contrast": "高对比度",
Expand Down
5 changes: 5 additions & 0 deletions frontend/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,8 @@
"activity_all_agents": "全部代理",
"activity_filter_by_machine": "按機器篩選",
"activity_all_machines": "全部機器",
"activity_filter_by_branch": "按分支篩選",
"activity_all_branches": "全部分支",
"activity_filter_by_automation": "按自動化篩選",
"activity_all_sessions": "全部會話",
"activity_interactive": "交互式",
Expand All @@ -788,6 +790,7 @@
"activity_project": "項目",
"activity_model": "模型",
"activity_agent": "代理",
"activity_branch": "分支",
"activity_min_unit": " 分鐘",
"activity_int_auto_split": "交互 {int} / 自動 {auto}",
"activity_breakdown": "細分",
Expand Down Expand Up @@ -1626,9 +1629,11 @@
"activity_insight_no_matching_agents": "無匹配代理",
"activity_filter_agents_placeholder": "篩選代理",
"activity_filter_machines_placeholder": "篩選機器",
"activity_filter_branches_placeholder": "篩選分支",
"activity_filter_automation_placeholder": "篩選自動化",
"activity_no_matching_agents": "無匹配代理",
"activity_no_matching_machines": "無匹配機器",
"activity_no_matching_branches": "無匹配分支",
"activity_no_automation_filters": "無自動化篩選",
"activity_loading_report": "正在加載活動報告...",
"appearance_high_contrast": "高對比度",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/api/generated/index.ts

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

15 changes: 15 additions & 0 deletions frontend/src/lib/api/generated/models/ActivityBranchKeyMinutes.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/generated/models/ActivityReport.ts

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

32 changes: 30 additions & 2 deletions frontend/src/lib/api/types/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,38 @@ import type {
ActivityReportInterval,
ActivitySessionRow,
ActivityKeyMinutes,
ActivityBranchKeyMinutes,
} from "../generated/index";

export type Report = ActivityReport;
export type Bucket = ActivityBucket;
export type ReportInterval = ActivityReportInterval;
export type SessionRow = ActivitySessionRow;
// models comes across as any[]; the codegen can't type OpenAPI 3.1's
// nullable-array items (see the Report comment below).
export type SessionRow = Omit<ActivitySessionRow, "models"> & {
models: string[] | null;
};
export type KeyMinutes = ActivityKeyMinutes;
export type BranchKeyMinutes = ActivityBranchKeyMinutes;

// Narrows the generated model's any[] | null collections to their element
// types; openapi-typescript-codegen can't type OpenAPI 3.1's nullable-array
// syntax (type: [T, "null"]) and falls back to any[]. Structurally
// compatible with ActivityReport, so responses need no runtime conversion.
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: BranchKeyMinutes[] | null;
by_model: KeyMinutes[] | null;
by_project: KeyMinutes[] | null;
by_session: SessionRow[] | null;
intervals: ReportInterval[] | null;
};
29 changes: 29 additions & 0 deletions frontend/src/lib/components/activity/ActivityPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import OptionTypeahead, {
type TypeaheadOption,
} from "../layout/OptionTypeahead.svelte";
import { branchLabel } from "../../branchFilters.js";
import {
addDays,
endOfMonth,
Expand Down Expand Up @@ -99,6 +100,17 @@
displayLabel: machine,
})),
]);
const branchOptions = $derived.by((): TypeaheadOption[] => [
{
name: "",
label: m.activity_all_branches(),
displayLabel: m.activity_all_branches(),
},
...activity.branches.map((b) => {
const label = branchLabel(b.project, b.branch, m.shared_no_branch());
return { name: b.token, label, displayLabel: label };
}),
]);
const automationOptions: TypeaheadOption[] = $derived([
{
name: "all",
Expand Down Expand Up @@ -255,6 +267,11 @@
activity.load();
}

function onBranchChange(value: string) {
activity.setBranch(value);
activity.load();
}

function onAutomationChange(value: string) {
activity.setAutomation(value as Automation);
activity.load();
Expand Down Expand Up @@ -337,6 +354,18 @@
/>
</div>

<div class="toolbar-typeahead">
<OptionTypeahead
options={branchOptions}
value={activity.branch}
fallbackLabel={m.activity_all_branches()}
placeholder={m.activity_filter_branches_placeholder()}
title={m.activity_filter_by_branch()}
emptyLabel={m.activity_no_matching_branches()}
onselect={onBranchChange}
/>
</div>

<div class="toolbar-typeahead compact">
<OptionTypeahead
options={automationOptions}
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/lib/components/activity/ActivityPage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ describe("ActivityPage refresh control layout", () => {
});
});

describe("ActivityPage branch filter", () => {
it("wires the branch typeahead through the activity store", () => {
expect(source).toContain("activity.branches.map");
expect(source).toContain("options={branchOptions}");
expect(source).toContain("value={activity.branch}");
expect(source).toContain("onselect={onBranchChange}");
expect(source).toContain("activity.setBranch(value)");
});
});

describe("ActivityPage date yoke controls", () => {
it("updates shared yoke state from the unified range picker", () => {
expect(source).toContain("<RangePicker");
Expand Down
Loading