diff --git a/cmd/agentsview/activity.go b/cmd/agentsview/activity.go index 3bcb40679..d5bb3a6ea 100644 --- a/cmd/agentsview/activity.go +++ b/cmd/agentsview/activity.go @@ -26,6 +26,7 @@ type ActivityReportConfig struct { Timezone string Bucket string Project string + Branch string Agent string Machine string JSON bool @@ -36,6 +37,12 @@ type ActivityReportConfig struct { // runActivityReport syncs, resolves the range, runs the report, and prints it. func runActivityReport(cfg ActivityReportConfig) { ctx := context.Background() + // Validate the (project, branch) flag combo up front so a bad combination + // fails before we resolve a backend, which may sync or start a daemon. + if _, err := branchFilterToken(cfg.Project, cfg.Branch); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } backend, cleanup, err := resolveArchiveQueryBackend(ctx, archiveQueryPolicy{ Offline: cfg.Offline, NoSync: cfg.NoSync, @@ -89,6 +96,11 @@ func fetchHTTPActivityReport( setIfNotEmpty("project", cfg.Project) setIfNotEmpty("agent", cfg.Agent) setIfNotEmpty("machine", cfg.Machine) + gitBranch, err := branchFilterToken(cfg.Project, cfg.Branch) + if err != nil { + return activity.Report{}, err + } + setIfNotEmpty("git_branch", gitBranch) endpoint := strings.TrimSuffix(tr.URL, "/") + "/api/v1/activity/report?" + q.Encode() @@ -159,9 +171,14 @@ func resolveActivityReport( return activity.Report{}, err } + gitBranch, err := branchFilterToken(cfg.Project, cfg.Branch) + if err != nil { + return activity.Report{}, err + } f := db.AnalyticsFilter{ Timezone: tz, Project: cfg.Project, + GitBranch: gitBranch, Agent: cfg.Agent, Machine: cfg.Machine, ExcludeOneShot: false, diff --git a/cmd/agentsview/cli.go b/cmd/agentsview/cli.go index f83d5008b..304dad23c 100644 --- a/cmd/agentsview/cli.go +++ b/cmd/agentsview/cli.go @@ -24,6 +24,18 @@ const ( const dataVersionTooNewExitCode = 3 +// branchFilterToken builds the (project, branch) filter token; a branch needs a +// project to scope it (like the MCP tools), so it errors without --project. +func branchFilterToken(project, branch string) (string, error) { + if branch == "" { + return "", nil + } + if project == "" { + return "", fmt.Errorf("--branch requires --project") + } + return db.EncodeBranchFilterToken(project, branch), nil +} + type cliExitError struct { code int err error @@ -518,6 +530,7 @@ func newActivityReportCommand() *cobra.Command { cmd.Flags().StringVar(&cfg.Timezone, "timezone", "", "IANA timezone for range bucketing") cmd.Flags().StringVar(&cfg.Bucket, "bucket", "", "Bucket size: 5m, 15m, 1h, 1d, 1w") cmd.Flags().StringVar(&cfg.Project, "project", "", "Filter by project") + cmd.Flags().StringVar(&cfg.Branch, "branch", "", "Filter by git branch name (requires --project)") cmd.Flags().StringVar(&cfg.Agent, "agent", "", "Filter by agent name") cmd.Flags().StringVar(&cfg.Machine, "machine", "", "Filter by machine name") registerFormatFlags(cmd.Flags()) diff --git a/cmd/agentsview/cli_test.go b/cmd/agentsview/cli_test.go index 6153d6efe..f5f944b90 100644 --- a/cmd/agentsview/cli_test.go +++ b/cmd/agentsview/cli_test.go @@ -316,3 +316,16 @@ func TestSyncHelpMentionsConfiguredHosts(t *testing.T) { assert.Contains(t, help, want, "sync help missing %q", want) } } + +func TestBranchFilterToken(t *testing.T) { + tok, err := branchFilterToken("proj", "") + require.NoError(t, err) + assert.Empty(t, tok, "empty branch yields no token") + + _, err = branchFilterToken("", "main") + assert.Error(t, err, "branch without project must error") + + tok, err = branchFilterToken("proj", "main") + require.NoError(t, err) + assert.Equal(t, db.EncodeBranchFilterToken("proj", "main"), tok) +} diff --git a/cmd/agentsview/session_get.go b/cmd/agentsview/session_get.go index 55ef21203..d40add05b 100644 --- a/cmd/agentsview/session_get.go +++ b/cmd/agentsview/session_get.go @@ -136,6 +136,9 @@ func printSessionDetailHuman(w io.Writer, s *service.SessionDetail) error { fmt.Fprintf(w, "%s %s\n", label("ID"), sanitizeTerminal(s.ID)) fmt.Fprintf(w, "%s %s\n", label("Name"), sanitizeTerminal(name)) fmt.Fprintf(w, "%s %s\n", label("Project"), sanitizeTerminal(s.Project)) + if s.GitBranch != "" { + fmt.Fprintf(w, "%s %s\n", label("Branch"), sanitizeTerminal(s.GitBranch)) + } fmt.Fprintf(w, "%s %s\n", label("Agent"), sanitizeTerminal(s.Agent)) fmt.Fprintf(w, "%s %s\n", label("Machine"), sanitizeTerminal(s.Machine)) fmt.Fprintf(w, "%s %s\n", diff --git a/cmd/agentsview/session_list.go b/cmd/agentsview/session_list.go index a2df60432..9d8a2c4cf 100644 --- a/cmd/agentsview/session_list.go +++ b/cmd/agentsview/session_list.go @@ -19,6 +19,7 @@ import ( func newSessionListCommand() *cobra.Command { var ( project, excludeProject, machine, agent string + branch string date, dateFrom, dateTo, activeSince string minMessages, maxMessages int minUserMessages int @@ -39,6 +40,10 @@ func newSessionListCommand() *cobra.Command { Args: cobra.NoArgs, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { + gitBranch, err := branchFilterToken(project, branch) + if err != nil { + return err + } svc, cleanup, err := resolveService(cmd) if err != nil { return err @@ -49,6 +54,7 @@ func newSessionListCommand() *cobra.Command { Project: project, ExcludeProject: excludeProject, Machine: machine, + GitBranch: gitBranch, Agent: agent, Date: date, DateFrom: dateFrom, @@ -123,6 +129,8 @@ func newSessionListCommand() *cobra.Command { "Exclude sessions from the given project") flags.StringVar(&machine, "machine", "", "Filter by machine name") + flags.StringVar(&branch, "branch", "", + "Filter by git branch name (requires --project)") flags.StringVar(&agent, "agent", "", "Filter by agent (claude, codex, cursor, ...)") flags.StringVar(&date, "date", "", diff --git a/cmd/agentsview/session_search.go b/cmd/agentsview/session_search.go index 54fff1f27..135d6f70a 100644 --- a/cmd/agentsview/session_search.go +++ b/cmd/agentsview/session_search.go @@ -18,7 +18,8 @@ func newSessionSearchCommand() *cobra.Command { in string excludeSystem, reveal bool project, excludeProject, agent string - machine, date, dateFrom, dateTo string + machine, branch string + date, dateFrom, dateTo string activeSince string includeChildren, includeAutomated bool includeOneShot bool @@ -54,6 +55,10 @@ func newSessionSearchCommand() *cobra.Command { case useFTS: mode = "fts" } + gitBranch, err := branchFilterToken(project, branch) + if err != nil { + return err + } svc, cleanup, err := resolveService(cmd) if err != nil { return err @@ -69,6 +74,7 @@ func newSessionSearchCommand() *cobra.Command { Project: project, ExcludeProject: excludeProject, Machine: machine, + GitBranch: gitBranch, Agent: agent, Date: date, DateFrom: dateFrom, @@ -105,6 +111,7 @@ func newSessionSearchCommand() *cobra.Command { flags.StringVar(&project, "project", "", "Filter by project name") flags.StringVar(&excludeProject, "exclude-project", "", "Exclude project") flags.StringVar(&machine, "machine", "", "Filter by machine") + flags.StringVar(&branch, "branch", "", "Filter by git branch name (requires --project)") flags.StringVar(&agent, "agent", "", "Filter by agent") flags.StringVar(&date, "date", "", "Sessions started on YYYY-MM-DD") flags.StringVar(&dateFrom, "date-from", "", "Sessions on or after YYYY-MM-DD") diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 8bc5bc2ed..bf5a263a1 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -378,6 +378,9 @@ "sidebar_filters_machine": "Machine", "sidebar_filters_search_machines": "Search machines...", "sidebar_filters_no_machines": "No machines", + "sidebar_filters_branch": "Branch", + "sidebar_filters_search_branches": "Search branches...", + "sidebar_filters_no_branches": "No branches", "sidebar_filters_min_prompts": "Min Prompts", "sidebar_filters_clear_filters": "Clear filters", "sidebar_row_expand": "Expand", @@ -396,6 +399,7 @@ "shared_active_filters_label": "Filters:", "shared_active_filters_clear_project": "Clear project filter", "shared_active_filters_remove_machine": "Remove {machine} filter", + "shared_active_filters_remove_branch": "Remove {branch} filter", "shared_active_filters_remove_agent": "Remove {agent} filter", "shared_active_filters_clear_min_prompts": "Clear min prompts filter", "shared_active_filters_min_prompts": "≥{count} prompts", @@ -460,6 +464,7 @@ "shared_no_sessions_in_range": "No sessions in range", "shared_none": "None", "shared_other": "Other", + "shared_no_branch": "(no branch)", "shared_unknown": "unknown", "analytics_refresh": "Refresh analytics", "analytics_export_csv": "Export CSV", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index dff7e35b1..1ac4ffac1 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -368,6 +368,9 @@ "sidebar_filters_machine": "Machine", "sidebar_filters_search_machines": "搜索 machines...", "sidebar_filters_no_machines": "无 machines", + "sidebar_filters_branch": "分支", + "sidebar_filters_search_branches": "搜索分支...", + "sidebar_filters_no_branches": "无分支", "sidebar_filters_min_prompts": "最少提示数", "sidebar_filters_clear_filters": "清除筛选器", "sidebar_row_expand": "展开", @@ -386,6 +389,7 @@ "shared_active_filters_label": "筛选器:", "shared_active_filters_clear_project": "清除项目筛选器", "shared_active_filters_remove_machine": "移除 {machine} 筛选器", + "shared_active_filters_remove_branch": "移除 {branch} 筛选器", "shared_active_filters_remove_agent": "移除 {agent} 筛选器", "shared_active_filters_clear_min_prompts": "清除最少提示数筛选器", "shared_active_filters_min_prompts": "≥{count} 条提示", @@ -449,6 +453,7 @@ "shared_no_sessions_in_range": "此范围内无会话", "shared_none": "无", "shared_other": "其他", + "shared_no_branch": "(无分支)", "shared_unknown": "未知", "analytics_refresh": "刷新分析", "analytics_export_csv": "导出 CSV", diff --git a/frontend/src/lib/api/generated/index.ts b/frontend/src/lib/api/generated/index.ts index d3bc43c74..fe5d02d95 100644 --- a/frontend/src/lib/api/generated/index.ts +++ b/frontend/src/lib/api/generated/index.ts @@ -18,6 +18,7 @@ export type { AgentsResponse } from './models/AgentsResponse'; export type { AgentTotal } from './models/AgentTotal'; export type { ApiErrorResponse } from './models/ApiErrorResponse'; export type { ApplyWorktreeMappingsResponse } from './models/ApplyWorktreeMappingsResponse'; +export type { BranchesResponse } from './models/BranchesResponse'; export type { BulkStarInputBody } from './models/BulkStarInputBody'; export type { CacheStats } from './models/CacheStats'; export type { Comparison } from './models/Comparison'; @@ -31,6 +32,7 @@ export type { DbAgentBreakdown } from './models/DbAgentBreakdown'; export type { DbAgentInfo } from './models/DbAgentInfo'; export type { DbAgentSummary } from './models/DbAgentSummary'; export type { DbAnalyticsSummary } from './models/DbAnalyticsSummary'; +export type { DbBranchInfo } from './models/DbBranchInfo'; export type { DbCallTiming } from './models/DbCallTiming'; export type { DbCategoryTotal } from './models/DbCategoryTotal'; export type { DbContentMatch } from './models/DbContentMatch'; diff --git a/frontend/src/lib/api/generated/models/BranchesResponse.ts b/frontend/src/lib/api/generated/models/BranchesResponse.ts new file mode 100644 index 000000000..b3eda7a99 --- /dev/null +++ b/frontend/src/lib/api/generated/models/BranchesResponse.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BranchesResponse = { + branches: any[] | null; +}; + diff --git a/frontend/src/lib/api/generated/models/DbBranchInfo.ts b/frontend/src/lib/api/generated/models/DbBranchInfo.ts new file mode 100644 index 000000000..0eb5262cb --- /dev/null +++ b/frontend/src/lib/api/generated/models/DbBranchInfo.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type DbBranchInfo = { + branch: string; + project: string; + token: string; +}; + diff --git a/frontend/src/lib/api/generated/services/ActivityService.ts b/frontend/src/lib/api/generated/services/ActivityService.ts index 9ffcbc038..bf4c2d2b9 100644 --- a/frontend/src/lib/api/generated/services/ActivityService.ts +++ b/frontend/src/lib/api/generated/services/ActivityService.ts @@ -20,6 +20,7 @@ export class ActivityService { timezone, bucket, project, + gitBranch, agent, machine, automation = 'all', @@ -52,6 +53,10 @@ export class ActivityService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -76,6 +81,7 @@ export class ActivityService { 'timezone': timezone, 'bucket': bucket, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'machine': machine, 'automation': automation, diff --git a/frontend/src/lib/api/generated/services/AnalyticsService.ts b/frontend/src/lib/api/generated/services/AnalyticsService.ts index 90076aeec..b0416c8d8 100644 --- a/frontend/src/lib/api/generated/services/AnalyticsService.ts +++ b/frontend/src/lib/api/generated/services/AnalyticsService.ts @@ -29,6 +29,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -61,6 +62,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -115,6 +120,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -153,6 +159,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -185,6 +192,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -239,6 +250,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -277,6 +289,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -308,6 +321,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -358,6 +375,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -395,6 +413,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -426,6 +445,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -476,6 +499,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -513,6 +537,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -544,6 +569,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -594,6 +623,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -632,6 +662,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -668,6 +699,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -722,6 +757,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -761,6 +797,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -792,6 +829,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -842,6 +883,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -879,6 +921,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -910,6 +953,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -960,6 +1007,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -997,6 +1045,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -1028,6 +1077,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -1078,6 +1131,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -1115,6 +1169,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -1146,6 +1201,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -1196,6 +1255,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -1233,6 +1293,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -1265,6 +1326,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -1319,6 +1384,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, @@ -1357,6 +1423,7 @@ export class AnalyticsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -1388,6 +1455,10 @@ export class AnalyticsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -1438,6 +1509,7 @@ export class AnalyticsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, diff --git a/frontend/src/lib/api/generated/services/MetadataService.ts b/frontend/src/lib/api/generated/services/MetadataService.ts index 6f215735d..71c9e4976 100644 --- a/frontend/src/lib/api/generated/services/MetadataService.ts +++ b/frontend/src/lib/api/generated/services/MetadataService.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { AgentsResponse } from '../models/AgentsResponse'; +import type { BranchesResponse } from '../models/BranchesResponse'; import type { DbStats } from '../models/DbStats'; import type { MachinesResponse } from '../models/MachinesResponse'; import type { ProjectsResponse } from '../models/ProjectsResponse'; @@ -52,6 +53,46 @@ export class MetadataService { }, }); } + /** + * List branches + * @returns BranchesResponse OK + * @throws ApiError + */ + public static getApiV1Branches({ + includeOneShot, + includeAutomated, + }: { + /** + * Include one-shot sessions + */ + includeOneShot?: boolean, + /** + * Include automated sessions + */ + includeAutomated?: boolean, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/branches', + query: { + 'include_one_shot': includeOneShot, + 'include_automated': includeAutomated, + }, + errors: { + 400: `Bad Request`, + 401: `Unauthorized`, + 403: `Forbidden`, + 404: `Not Found`, + 409: `Conflict`, + 422: `Unprocessable Entity`, + 500: `Internal Server Error`, + 501: `Not Implemented`, + 502: `Bad Gateway`, + 503: `Service Unavailable`, + 504: `Gateway Timeout`, + }, + }); + } /** * List machines * @returns MachinesResponse OK diff --git a/frontend/src/lib/api/generated/services/SearchService.ts b/frontend/src/lib/api/generated/services/SearchService.ts index 7d27e4de4..90b04932c 100644 --- a/frontend/src/lib/api/generated/services/SearchService.ts +++ b/frontend/src/lib/api/generated/services/SearchService.ts @@ -80,6 +80,7 @@ export class SearchService { project, excludeProject, machine, + gitBranch, agent, date, dateFrom, @@ -123,6 +124,10 @@ export class SearchService { * Filter by machine */ machine?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -176,6 +181,7 @@ export class SearchService { 'project': project, 'exclude_project': excludeProject, 'machine': machine, + 'git_branch': gitBranch, 'agent': agent, 'date': date, 'date_from': dateFrom, diff --git a/frontend/src/lib/api/generated/services/SessionsService.ts b/frontend/src/lib/api/generated/services/SessionsService.ts index e865eda86..e0031d095 100644 --- a/frontend/src/lib/api/generated/services/SessionsService.ts +++ b/frontend/src/lib/api/generated/services/SessionsService.ts @@ -59,6 +59,7 @@ export class SessionsService { project, excludeProject, machine, + gitBranch, agent, date, dateFrom, @@ -93,6 +94,10 @@ export class SessionsService { * Filter by machine */ machine?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -185,6 +190,7 @@ export class SessionsService { 'project': project, 'exclude_project': excludeProject, 'machine': machine, + 'git_branch': gitBranch, 'agent': agent, 'date': date, 'date_from': dateFrom, @@ -231,6 +237,7 @@ export class SessionsService { project, excludeProject, machine, + gitBranch, agent, date, dateFrom, @@ -265,6 +272,10 @@ export class SessionsService { * Filter by machine */ machine?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -357,6 +368,7 @@ export class SessionsService { 'project': project, 'exclude_project': excludeProject, 'machine': machine, + 'git_branch': gitBranch, 'agent': agent, 'date': date, 'date_from': dateFrom, diff --git a/frontend/src/lib/api/generated/services/TrendsService.ts b/frontend/src/lib/api/generated/services/TrendsService.ts index 47b0cea01..9fcbd062e 100644 --- a/frontend/src/lib/api/generated/services/TrendsService.ts +++ b/frontend/src/lib/api/generated/services/TrendsService.ts @@ -18,6 +18,7 @@ export class TrendsService { timezone, machine, project, + gitBranch, agent, model, dow, @@ -51,6 +52,10 @@ export class TrendsService { * Filter by project */ project?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Filter by agent */ @@ -109,6 +114,7 @@ export class TrendsService { 'timezone': timezone, 'machine': machine, 'project': project, + 'git_branch': gitBranch, 'agent': agent, 'model': model, 'dow': dow, diff --git a/frontend/src/lib/api/generated/services/UsageService.ts b/frontend/src/lib/api/generated/services/UsageService.ts index 3ba2fec33..ef30894b1 100644 --- a/frontend/src/lib/api/generated/services/UsageService.ts +++ b/frontend/src/lib/api/generated/services/UsageService.ts @@ -21,6 +21,7 @@ export class UsageService { agent, project, machine, + gitBranch, excludeProject, excludeAgent, excludeModel, @@ -62,6 +63,10 @@ export class UsageService { * Filter by machine */ machine?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Exclude a project */ @@ -121,6 +126,7 @@ export class UsageService { 'agent': agent, 'project': project, 'machine': machine, + 'git_branch': gitBranch, 'exclude_project': excludeProject, 'exclude_agent': excludeAgent, 'exclude_model': excludeModel, @@ -162,6 +168,7 @@ export class UsageService { agent, project, machine, + gitBranch, excludeProject, excludeAgent, excludeModel, @@ -199,6 +206,10 @@ export class UsageService { * Filter by machine */ machine?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Exclude a project */ @@ -258,6 +269,7 @@ export class UsageService { 'agent': agent, 'project': project, 'machine': machine, + 'git_branch': gitBranch, 'exclude_project': excludeProject, 'exclude_agent': excludeAgent, 'exclude_model': excludeModel, @@ -298,6 +310,7 @@ export class UsageService { agent, project, machine, + gitBranch, excludeProject, excludeAgent, excludeModel, @@ -336,6 +349,10 @@ export class UsageService { * Filter by machine */ machine?: string, + /** + * Filter by git branch; opaque (project, branch) tokens from the /branches endpoint + */ + gitBranch?: string, /** * Exclude a project */ @@ -399,6 +416,7 @@ export class UsageService { 'agent': agent, 'project': project, 'machine': machine, + 'git_branch': gitBranch, 'exclude_project': excludeProject, 'exclude_agent': excludeAgent, 'exclude_model': excludeModel, diff --git a/frontend/src/lib/api/types/core.ts b/frontend/src/lib/api/types/core.ts index d0b0d3bfe..5014f1a1b 100644 --- a/frontend/src/lib/api/types/core.ts +++ b/frontend/src/lib/api/types/core.ts @@ -197,6 +197,17 @@ export interface MachinesResponse { machines: string[]; } +/** Matches Go BranchInfo struct in internal/db/sessions.go */ +export interface BranchInfo { + project: string; + branch: string; + token: string; +} + +export interface BranchesResponse { + branches: BranchInfo[]; +} + /** Matches Go AgentInfo struct */ export interface AgentInfo { name: string; diff --git a/frontend/src/lib/branchFilters.test.ts b/frontend/src/lib/branchFilters.test.ts new file mode 100644 index 000000000..89443becb --- /dev/null +++ b/frontend/src/lib/branchFilters.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { + branchFilterToken, + branchLabel, + branchTokenLabel, +} from "./branchFilters.js"; + +describe("branch filter labels", () => { + it("keeps empty branch labels distinct from a real unknown branch", () => { + const noBranch = "(no branch)"; + expect(branchLabel("proj", "", noBranch)).toBe("proj/(no branch)"); + expect(branchLabel("proj", "unknown", noBranch)).toBe("proj/unknown"); + expect(branchTokenLabel(branchFilterToken("proj", ""), noBranch)).toBe( + "proj/(no branch)", + ); + expect(branchTokenLabel( + branchFilterToken("proj", ""), + "No branch", + )).toBe("proj/No branch"); + }); +}); diff --git a/frontend/src/lib/branchFilters.ts b/frontend/src/lib/branchFilters.ts new file mode 100644 index 000000000..2e8c26cfc --- /dev/null +++ b/frontend/src/lib/branchFilters.ts @@ -0,0 +1,34 @@ +// Keep these in sync with internal/db branch token separators. +export const BRANCH_TOKEN_SEP = "\u001f"; +export const BRANCH_LIST_SEP = "\u001e"; + +export function branchFilterToken(project: string, branch: string): string { + return project + BRANCH_TOKEN_SEP + branch; +} + +export function splitBranchFilterToken(token: string): { + project: string; + branch: string; +} { + const i = token.indexOf(BRANCH_TOKEN_SEP); + return i < 0 + ? { project: "", branch: token } + : { project: token.slice(0, i), branch: token.slice(i + 1) }; +} + +export function branchLabel( + project: string, + branch: string, + noBranchLabel: string, +): string { + const label = branch || noBranchLabel; + return project ? `${project}/${label}` : label; +} + +export function branchTokenLabel( + token: string, + noBranchLabel: string, +): string { + const { project, branch } = splitBranchFilterToken(token); + return branchLabel(project, branch, noBranchLabel); +} diff --git a/frontend/src/lib/components/filters/SessionActiveFilters.svelte b/frontend/src/lib/components/filters/SessionActiveFilters.svelte index 2886494ee..8b6901dce 100644 --- a/frontend/src/lib/components/filters/SessionActiveFilters.svelte +++ b/frontend/src/lib/components/filters/SessionActiveFilters.svelte @@ -1,6 +1,7 @@