Skip to content

Commit 5513156

Browse files
peyton-altclaude
andauthored
feat: entire recap command (#1015)
* feat(recap): introduce entire recap command and package Introduces the 'entire recap' CLI command that shows checkpoint activity across day/week/month/90d ranges. Local session loading, api enrichment cache, view builder, heatmap, badges, aggregators, contributors fetch from the repo-overview endpoints, TUI + static renderers. This is the baseline from which follow-up commits wire /api/v1/me/recap, redesign the visual layout with comparison bars, and add the view toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: a8b9ab719bdc * feat(recap): wire CLI to /api/v1/me/recap endpoint Pairs with entirehq/entire.io#1604. Prefers the consolidated /me/recap endpoint when available; falls back to the three-endpoint stack when the server doesn't have /me/recap yet. - Adds me_recap.go with FetchMeRecap, TimeframeForRange, and ContributorsFromMeRecap for the fast-path client. - Adds stampRepos so cp.Repo is populated for local checkpoints. - Adds hasUsefulSignal to invalidate cache entries with empty payloads. - Adds AnalysisStatusComplete guard in EnrichCheckpoint to skip pending or failed rows instead of poisoning the cache with blanks. - Adds SkillsUsed/MCPServers to RecapCheckpoint + CheckpointAnalysisResponse. - Threads Notes diagnostic footer through View. - JSON-tags LabelCount so musttag accepts it in decode paths. - Uses repoUnknown constant in aggregate_test.go (goconst fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 0de0ce0a0421 * feat(recap): remove --30 flag and Range30d enum --30 rolling-30-days overlaps with --month (this calendar month) for practical purposes. Range30d had no persisted state (feature hasn't shipped), so no migration needed. Users who type --30 get cobra's native unknown-flag error with suggestion pointing at --90 or --month. Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 0d929f3248b7 * fix(recap): include recap_view.go in Range30d removal (pre-commit hook miss) Entire-Checkpoint: c9967a53a49b * feat(recap): add team style + renderComparisonBar primitive Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Agents panel spec Plan Chunk 2. Adds: - team lipgloss style (magenta 170, bold) for contributors column accent - renderComparisonBar(you, team, width, styles) primitive with 9 TDD tests covering: you>team, team>you, equal (striped), one-sided (both directions), tiny-vs-huge rounding floor, both-zero drop, narrow/wide width clamping. No wiring yet — Chunk 5 will integrate this into the agents card. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: 4db4bb19673f * fix(recap): restore barMinWidth=12 per spec The Equal test was using width=10 which contradicted the spec's min=12. Keep the spec constant and update the test to use width=12 (and expect 6+6 striped cells, not 5+5). Entire-Checkpoint: 8968e65373ea * feat(recap): rewrite summary panel (you/team/top/context) Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Summary Plan Chunk 3. Drops the Top agent / Top skill / Focus / Top model placeholder rows. New top line reflows when signals are empty; disappears when all four are empty. You/team split mirrors the Agents panel framing. Moves summary rendering into its own file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: 9837c81fcc5a * feat(recap): pad activity strip + add peak annotation Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Activity Plan Chunk 4. Renders every cell (including zero days) so the strip fills its full width. Anchors rightmost to today. Adds 'most active: <time>' annotation to the top-right (hourly for --day, 'Jan 2' for longer ranges). Moves activity rendering into its own file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: 780df16ce84f * feat(recap): rewrite agents card with bars + view toggle Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Agents Plan Chunk 5. Per-agent card now has 3 bar rows (tokens/sessions/checkpoints) + team qualitative block + your qualitative block, each skipped when irrelevant. Drops MCP servers and Distinct contribs per spec. Alphabetical tie-break on combined-sessions sort for snapshot determinism. Panel-level you/team legend replaces per-card headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: 9abf315e5966 * feat(recap): drop bottom panel (worktrees · labels · repos) Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Page composition Plan Chunk 6. Labels already inside agent cards (team labels rows). Repos inside agent cards too (your repos). Worktrees adds no daily value. Three-panel output (summary → activity → agents) is the new shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: ee3fef263e39 * feat(recap): add v keybinding to cycle view mode in TUI Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Header bar Plan Chunk 7. Cycles ViewBoth → ViewMe → ViewContributors → ViewBoth. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: 83ad8e15f554 * feat(recap): readout-only fallback for narrow terminals Spec: docs/2026-04-22-entire-recap-visual-redesign-design.md §Minimum terminal width. Plan Chunk 8. When the bar drops below barMinWidth (12 cells) the primitive returns empty; the card row renders just the numeric readout. Preserves usability on <70-col terminals. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Entire-Checkpoint: 07f7775d23d8 * fix(recap): session-level tokens, dedupe title, weight top-agent by checkpoints - Tokens now read from s.TokenUsage (session-level) instead of the always-nil cp.TokenUsage. projectCheckpoint in load.go intentionally leaves cp.TokenUsage unset; the aggregators hadn't been updated. - renderSummaryBand no longer duplicates the range title — renderPanel already shows view.Title, so the inner title line was a double. - Top-agent calculation now weights each session by 1 + len(Checkpoints) so an agent with 23 sessions but 0 checkpoints doesn't outrank one with 15 sessions and 106 checkpoints. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: a6117ab65f84 * refactor(recap): drop legacy contributors fallback now that /me/recap ships Removes FetchContributors and the three-endpoint merge stack (agent-activity + contributor-agents + contributor-tokens) since the consolidated /api/v1/me/recap endpoint landed in entirehq/entire.io#1604. Keeps ContributorsData + AgentContrib as the shared shape — still used by the me_recap.go adapter and the view renderer. Entire-Checkpoint: 95a5057707a2 * fix(recap): match mockup — wire team qualitative rows + label dots - applyContributors was dropping ContribLabels/Skills/ToolMix from the /me/recap response; those fields now flow into AgentCard so team labels/skills/tool mix rows actually render. - Labels use the mockup's ● bullet with semantic color instead of '<label> <pct>%' — the percentage was noise. - Agents panel title now includes the range ('Agents · 90d', etc.) per the mockup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 77908e4c5ac6 * refactor(recap): drop dead code + register recap in root.go Removes 4 unused aggregators (AggregateRange, AggregateByDay, AggregateByAgent, AggregateByRepo) and the 4 summary types that only they consumed (RecapRangeSummary, RecapAgentSummary, RecapRepoSummary, RecapDay). Drops heatmap.go entirely (4 unused exports left from an abandoned local heatmap path) and tool_categorize.go (unused local tool-time classifier now that tool-time comes from the server). Also fixes a missed registration: newRecapCmd() was never wired into root.go in the prior foundation commit, so `entire recap` wouldn't appear in --help or execute. Adds the one-line AddCommand. Net -674 lines in the recap package. Entire-Checkpoint: 97097f304a34 * feat(recap): wire me-side server data through view + TUI The team-side aggregates (labels, skills, tool mix) already flowed from /api/v1/me/recap into the agents card via b15d9d4. This mirrors the same plumbing on the me-side so the user's own row matches the dashboard counts instead of being recomputed from local checkpoint state. What changes: - AgentCard gains MeLabels / MeSkills / MeToolMix, populated by a new applyServerMe pass that overlays /me/recap's me-side aggregates on the locally-bucketed cards. - recap.go's fetchContributorsWithDiag becomes fetchRecapDataWithDiag, returning (serverMe, contributors, daily, diag) in one round trip with explicit since/until bounds derived from the CLI's range — replacing the looser TimeframeForRange handoff. - BuildOpts gains ServerMe + ServerDaily; runRecapTUI/NewTUIModel thread them through so the v / 1-4 keybindings can rebuild the view from the cached fetch instead of re-hitting the server. - me_recap.go gains MeFromMeRecap, parallel to ContributorsFromMeRecap. The static + activity renderers prefer server daily counts when available, falling back to local aggregation when offline. - Two new tests pin formatToolMix's normalization of raw category counts to top-3 percentage bars. Pairs with entirehq/entire.io#1604 (now perf-fixed via Promise.all). MCP servers stay deliberately unrendered per c990a5e. Entire-Checkpoint: d69c684aa89d * fix(recap): wrap TUI in viewport so output scrolls instead of clipping The three-panel rendered output (summary + activity strip + agents card) regularly exceeds the visible terminal height, especially when the agents card lists 4+ agents with bars and qualitative rows. The previous TUIModel handed the full string straight to bubbletea, which clipped at the bottom and offered no scroll affordance — users had to shrink their terminal font to see the rest. Wraps the rendered body in a bubbles `viewport.Model`: - ↑/↓, j/k, page up/down, mouse wheel scroll within the rendered output - Help line stays pinned outside the viewport so the keybind hint is always visible regardless of scroll position - Range/agent/view-mode toggles refresh viewport content and snap scroll back to the top so the summary is always seen first - Pre-WindowSizeMsg renders a static fallback for the brief flash before bubbletea sends the initial size; viewport takes over on first message Mirrors the viewport pattern already used in activity_tui.go. Entire-Checkpoint: 7f3827580375 * perf(recap): prefetch all ranges in parallel + clear stale data on miss Range toggles (d/w/m/4) used to either show frozen day-1 data (server fields cached forever) or display the previous range's numbers under the new range's title (after we added refetch but before we cleared stale fields). This wires the TUI through a proper cache: - On startup, seed cache[initialRange] with the synchronous fetch's payload, then fire prefetches for the other 3 ranges in parallel via tea.Batch. After ~1-5s background round-trip every range toggle is a cache hit. - On range toggle: cache hit -> swap data, instant render. Cache miss with no in-flight fetch -> clear server fields (so we don't display the previous range's numbers under the new title), mark loading, fire the fetch. Cache miss with in-flight fetch -> mark loading and wait for the existing reply. - Active days now reads from server daily counts when present, matching the dashboard cross-worktree count instead of undercounting from local sessions. Pairs with entirehq/entire.io#1604 server-side perf work. Entire-Checkpoint: 8ac353ca5a57 * style(recap): gofmt tui.go field alignment CI's gofmt check caught struct field misalignment in TUIModel that my local lint missed (we'd run gofmt -s -w on a formatted tree, but this section needed re-alignment after the cache+inFlight fields were added). gofmt -s -d output was empty locally yet CI rejected; rerunning gofmt -w produced this 3-line whitespace fix. Entire-Checkpoint: 288a91aff142 * Refactor recap to server-backed static renderer Entire-Checkpoint: beb9e66203f5 * Allow intentional interface returns * Color recap labels and skills * Fix concurrent session state saves --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 242275a commit 5513156

13 files changed

Lines changed: 1327 additions & 43 deletions

File tree

.golangci.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ linters:
115115
- github.com/go-git/go-git/v6/plumbing.EncodedObject
116116
- github.com/go-git/go-git/v6/storage.Storer
117117
- github.com/go-git/go-git/v6/plumbing/storer.EncodedObjectIter
118+
- github.com/go-git/go-git/v6/x/plugin.Signer
119+
- golang.org/x/crypto/ssh/agent.Agent
120+
- github.com/entireio/cli/cmd/entire/cli/summarize.Generator
118121
- github.com/entireio/cli/e2e/agents.Session
119122
- github.com/go-git/go-billy/v6.Filesystem
120123
- golang.org/x/crypto/ssh/agent.Agent

cmd/entire/cli/recap.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"os"
9+
"strings"
10+
"time"
11+
12+
"github.com/mattn/go-isatty"
13+
"github.com/spf13/cobra"
14+
"golang.org/x/term"
15+
16+
"github.com/entireio/cli/cmd/entire/cli/gitremote"
17+
"github.com/entireio/cli/cmd/entire/cli/paths"
18+
"github.com/entireio/cli/cmd/entire/cli/recap"
19+
)
20+
21+
type recapFlags struct {
22+
day, week, month, d90 bool
23+
view string
24+
agent string
25+
color string
26+
insecureHTTP bool
27+
}
28+
29+
const (
30+
recapColorAuto = "auto"
31+
recapColorAlways = "always"
32+
recapColorNever = "never"
33+
)
34+
35+
func newRecapCmd() *cobra.Command {
36+
f := &recapFlags{view: string(recap.ViewBoth), agent: recap.AgentAll, color: recapColorAuto}
37+
cmd := &cobra.Command{
38+
Use: "recap",
39+
Short: "Summarize recent checkpoint activity",
40+
RunE: func(cmd *cobra.Command, _ []string) error {
41+
return runRecap(cmd.Context(), cmd.OutOrStdout(), f)
42+
},
43+
}
44+
cmd.Flags().BoolVar(&f.day, "day", false, "Today only (default)")
45+
cmd.Flags().BoolVar(&f.week, "week", false, "Last 7 days")
46+
cmd.Flags().BoolVar(&f.month, "month", false, "This calendar month")
47+
cmd.Flags().BoolVar(&f.d90, "90", false, "Rolling 90 days")
48+
cmd.Flags().StringVar(&f.agent, "agent", recap.AgentAll, "Agent id to show, or all")
49+
cmd.Flags().StringVar(&f.view, "view", string(recap.ViewBoth), "Which columns to show: you, team, or both")
50+
cmd.Flags().StringVar(&f.color, "color", recapColorAuto, "Color output: auto, always, or never")
51+
cmd.Flags().BoolVar(&f.insecureHTTP, "insecure-http-auth", false, "Allow plain-HTTP auth (local dev only)")
52+
cmd.MarkFlagsMutuallyExclusive("day", "week", "month", "90")
53+
return cmd
54+
}
55+
56+
func (f *recapFlags) rangeKey() recap.RangeKey {
57+
switch {
58+
case f.week:
59+
return recap.RangeWeek
60+
case f.month:
61+
return recap.RangeMonth
62+
case f.d90:
63+
return recap.Range90d
64+
default:
65+
return recap.RangeDay
66+
}
67+
}
68+
69+
func (f *recapFlags) mode() recap.ViewMode {
70+
switch strings.ToLower(strings.TrimSpace(f.view)) {
71+
case "you", "me":
72+
return recap.ViewYou
73+
case "team", "contributors":
74+
return recap.ViewTeam
75+
case "both", "":
76+
return recap.ViewBoth
77+
default:
78+
return recap.ViewMode(f.view)
79+
}
80+
}
81+
82+
func (f *recapFlags) agentName() string {
83+
agent := strings.ToLower(strings.TrimSpace(f.agent))
84+
if agent == "" {
85+
return recap.AgentAll
86+
}
87+
return agent
88+
}
89+
90+
func (f *recapFlags) colorEnabled(w io.Writer) (bool, error) {
91+
switch strings.ToLower(strings.TrimSpace(f.color)) {
92+
case "", recapColorAuto:
93+
return shouldUseColor(w) && !IsAccessibleMode(), nil
94+
case recapColorAlways:
95+
return true, nil
96+
case recapColorNever:
97+
return false, nil
98+
default:
99+
return false, fmt.Errorf("invalid --color %q (use auto, always, or never)", f.color)
100+
}
101+
}
102+
103+
func runRecap(ctx context.Context, w io.Writer, f *recapFlags) error {
104+
if _, err := paths.WorktreeRoot(ctx); err != nil {
105+
fmt.Fprintln(w, "Not a git repository. Run 'entire recap' from within a git repository.")
106+
return NewSilentError(errors.New("not a git repository"))
107+
}
108+
mode := f.mode()
109+
if !mode.Valid() {
110+
return fmt.Errorf("invalid --view %q (use you, team, or both)", f.view)
111+
}
112+
color, err := f.colorEnabled(w)
113+
if err != nil {
114+
return err
115+
}
116+
client, err := NewAuthenticatedAPIClient(f.insecureHTTP)
117+
if err != nil {
118+
fmt.Fprintln(w, "Sign in with `entire login` to use `entire recap`.")
119+
return NewSilentError(err)
120+
}
121+
rangeKey := f.rangeKey()
122+
start, end := rangeKey.Bounds(time.Now())
123+
resp, err := recap.FetchMeRecap(ctx, client, start, end, currentRepoSlug(ctx), 0)
124+
if err != nil {
125+
return fmt.Errorf("fetch recap: %w", err)
126+
}
127+
fmt.Fprint(w, recap.RenderStaticRecap(resp, recap.RenderOptions{
128+
Range: rangeKey,
129+
View: mode,
130+
Agent: f.agentName(),
131+
Width: terminalWidth(w),
132+
Color: color,
133+
}))
134+
fmt.Fprintln(w)
135+
return nil
136+
}
137+
138+
func terminalWidth(w io.Writer) int {
139+
file, ok := w.(*os.File)
140+
if !ok {
141+
return recap.DefaultWidth
142+
}
143+
if !isatty.IsTerminal(file.Fd()) {
144+
return recap.DefaultWidth
145+
}
146+
width, _, err := term.GetSize(int(file.Fd())) //nolint:gosec // fd values fit in int on supported platforms
147+
if err != nil || width <= 0 {
148+
return recap.DefaultWidth
149+
}
150+
return width
151+
}
152+
153+
func currentRepoSlug(ctx context.Context) string {
154+
_, owner, repoName, err := gitremote.ResolveRemoteRepo(ctx, "origin")
155+
if err != nil || owner == "" || repoName == "" {
156+
return ""
157+
}
158+
return owner + "/" + repoName
159+
}

cmd/entire/cli/recap/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package recap contains the server-backed data types and static renderer
2+
// behind `entire recap`.
3+
package recap

cmd/entire/cli/recap/me_recap.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package recap
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net/url"
10+
"strconv"
11+
"time"
12+
13+
"github.com/entireio/cli/cmd/entire/cli/api"
14+
)
15+
16+
// MeRecapResponse mirrors GET /api/v1/me/recap.
17+
type MeRecapResponse struct {
18+
Timeframe string `json:"timeframe"`
19+
Repo *string `json:"repo"`
20+
Since string `json:"since"`
21+
Until string `json:"until"`
22+
Agents map[string]AgentEntry `json:"agents"`
23+
Summary Summary `json:"summary"`
24+
Contributors *ContribSummary `json:"contributors"`
25+
Daily []DailyCount `json:"daily"`
26+
UpdatedAt string `json:"updated_at"`
27+
}
28+
29+
// Summary contains top-level counts intended for CLI rendering.
30+
type Summary struct {
31+
Me SummaryTotals `json:"me"`
32+
Team *SummaryTotals `json:"team"`
33+
RepoCount int `json:"repoCount"`
34+
ActiveDays int `json:"activeDays"`
35+
Analysis AnalysisStatus `json:"analysis"`
36+
}
37+
38+
type SummaryTotals struct {
39+
Sessions int `json:"sessions"`
40+
Checkpoints int `json:"checkpoints"`
41+
Tokens int `json:"tokens"`
42+
}
43+
44+
type AnalysisStatus struct {
45+
Complete int `json:"complete"`
46+
Pending int `json:"pending"`
47+
Failed int `json:"failed"`
48+
}
49+
50+
type DailyCount struct {
51+
Date string `json:"date"`
52+
Count int `json:"count"`
53+
}
54+
55+
type AgentEntry struct {
56+
AgentID string `json:"agentId"`
57+
AgentLabel string `json:"agentLabel"`
58+
Me AgentAggregate `json:"me"`
59+
Contributors *AgentAggregate `json:"contributors"`
60+
}
61+
62+
type AgentAggregate struct {
63+
Sessions int `json:"sessions"`
64+
Checkpoints int `json:"checkpoints"`
65+
Tokens int `json:"tokens"`
66+
TranscriptTokens int `json:"transcriptTokens"`
67+
FilesChanged int `json:"filesChanged"`
68+
Labels []LabelCount `json:"labels"`
69+
Skills []SkillCount `json:"skills"`
70+
MCPServers []McpCount `json:"mcpServers"`
71+
ToolMix ToolMix `json:"toolMix"`
72+
}
73+
74+
type LabelCount struct {
75+
Label string `json:"label"`
76+
Count int `json:"count"`
77+
}
78+
79+
type SkillCount struct {
80+
Skill string `json:"skill"`
81+
Count int `json:"count"`
82+
}
83+
84+
type McpCount struct {
85+
Name string `json:"name"`
86+
Count int `json:"count"`
87+
}
88+
89+
type ToolMix struct {
90+
Shell int `json:"shell"`
91+
FileOps int `json:"fileOps"`
92+
Search int `json:"search"`
93+
MCP int `json:"mcp"`
94+
Agent int `json:"agent"`
95+
Other int `json:"other"`
96+
}
97+
98+
type ContribSummary struct {
99+
DistinctUsers int `json:"distinctUsers"`
100+
TotalTokens int `json:"totalTokens"`
101+
TotalCheckpoints int `json:"totalCheckpoints"`
102+
}
103+
104+
// FetchMeRecap fetches one server-backed recap window.
105+
func FetchMeRecap(
106+
ctx context.Context,
107+
client *api.Client,
108+
since, until time.Time,
109+
repo string,
110+
limit int,
111+
) (*MeRecapResponse, error) {
112+
if client == nil {
113+
return nil, errors.New("me/recap: nil client")
114+
}
115+
q := url.Values{}
116+
q.Set("since", since.UTC().Format(time.RFC3339))
117+
q.Set("until", until.UTC().Format(time.RFC3339))
118+
if repo != "" {
119+
q.Set("repo", repo)
120+
}
121+
if limit > 0 {
122+
q.Set("limit", strconv.Itoa(limit))
123+
}
124+
resp, err := client.Get(ctx, "/api/v1/me/recap?"+q.Encode())
125+
if err != nil {
126+
return nil, fmt.Errorf("me/recap get: %w", err)
127+
}
128+
defer func() { _ = resp.Body.Close() }()
129+
if resp.StatusCode >= 400 {
130+
body, _ := io.ReadAll(resp.Body) //nolint:errcheck // best-effort error body
131+
return nil, fmt.Errorf("me/recap: http %d: %s", resp.StatusCode, string(body))
132+
}
133+
var out MeRecapResponse
134+
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
135+
return nil, fmt.Errorf("me/recap decode: %w", err)
136+
}
137+
return &out, nil
138+
}

cmd/entire/cli/recap/model.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package recap
2+
3+
import "time"
4+
5+
// RangeKey names the static recap windows supported by `entire recap`.
6+
type RangeKey string
7+
8+
const (
9+
RangeDay RangeKey = "day"
10+
RangeWeek RangeKey = "week"
11+
RangeMonth RangeKey = "month"
12+
Range90d RangeKey = "90d"
13+
)
14+
15+
// Title returns the panel title for a range.
16+
func (r RangeKey) Title() string {
17+
switch r {
18+
case RangeDay:
19+
return "Today"
20+
case RangeWeek:
21+
return "Last 7 days"
22+
case RangeMonth:
23+
return "This month"
24+
case Range90d:
25+
return "Last 90 days"
26+
default:
27+
return "Today"
28+
}
29+
}
30+
31+
// Bounds returns a half-open [start, end) window in the user's local time.
32+
func (r RangeKey) Bounds(now time.Time) (time.Time, time.Time) {
33+
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
34+
dayEnd := dayStart.AddDate(0, 0, 1)
35+
switch r {
36+
case RangeDay:
37+
return dayStart, dayEnd
38+
case RangeWeek:
39+
return dayEnd.AddDate(0, 0, -7), dayEnd
40+
case RangeMonth:
41+
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
42+
return monthStart, monthStart.AddDate(0, 1, 0)
43+
case Range90d:
44+
return dayEnd.AddDate(0, 0, -90), dayEnd
45+
default:
46+
return dayStart, dayEnd
47+
}
48+
}
49+
50+
// ViewMode selects which columns render in the static agents panel.
51+
type ViewMode string
52+
53+
const (
54+
ViewYou ViewMode = "you"
55+
ViewTeam ViewMode = "team"
56+
ViewBoth ViewMode = "both"
57+
)
58+
59+
// Valid reports whether the mode is one of the supported static modes.
60+
func (m ViewMode) Valid() bool {
61+
return m == ViewYou || m == ViewTeam || m == ViewBoth
62+
}

0 commit comments

Comments
 (0)