# cli: show recent sessions (not commits) in `entire activity`

`887f764`→[main](/content/gh/entireio/cli/commits/main/index.html)·

Soph·1w ago·8 files·+604 added/-74 removed

The entire.io Overview page renders a recent-sessions feed from
GET /api/v1/me/sessions, but `entire activity` rendered a commits feed
from /me/commits, so the two surfaces listed different things.

Make sessions the default recent-list in `entire activity`, matching the
web: fetch /me/sessions (limit 50, mirroring the web's
USER_OVERVIEW_RECENT_SESSIONS_LIMIT), group by local day of lastActivityAt
(newest first), and render one row per session with the same fields the
web row shows — display name, repo, public tag, agent badge, friendly
model label, and checkpoint count. The stat cards, contribution heatmap,
and repo chart (all from /me/activity) are unchanged.

Commits stay available behind `entire activity --commits`.

- Port entire.io's formatModel (frontend/src/lib/model.ts) to Go so model
labels read identically (claude-opus-4-6 -> "Opus 4.6", gpt-4o ->
"GPT-4o", gemini-2.0-flash -> "Gemini 2.0 Flash").
- Extract the shared always-fetch-activity-plus-a-list plumbing
(fetchActivityWith) and day-grouping (groupItemsByDay) so the commit and
session paths don't duplicate it.
- Routes through the existing runAuthenticatedActivityAPI, so /me/sessions
go to the caller's home entire-api cell with the data-API fallback.

Stacked on #1641.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Sessions

5df0b30804c2View transcript

[?\
Fix Sessions Endpoint in Activity FeedClaude Code·Opus 4.8[1m]·3 steps](/content/gh/entireio/cli/session/1b0292dc-2cb1-49e9-91f5-41188abf9147#timeline-5df0b30804c2/index.html)

## Changes

8

- cmd/entire/cli

- Mactivity_cmd.go+109/-39

- Mactivity_cmd_test.go+2/-2

- Mactivity_render.go+111/-2

- Aactivity_sessions_test.go+178

- Mactivity_tui.go+50/-31

- Mactivity_types.go+34

- Amodel_label.go+78

- Amodel_label_test.go+42

```  
21 unmodified lines
```

```  
39 unmodified lines
```

```  
90 unmodified lines
```

```  
80 unmodified lines
```

package cli

import (
	"regexp"
	"strings"
)

// claudeModelRe matches Claude identifiers: claude-<family>-<major>[-<tail>].
// The tail (rest) carries optional minor versions and/or a legacy date suffix.
var claudeModelRe = regexp.MustCompile(`(?i)^claude-(opus|sonnet|haiku)-(1)(.*)$`)

// dateSuffixRe matches a legacy date chunk (6+ digits, e.g. 20250514).
var dateSuffixRe = regexp.MustCompile(`^\d{6,}$`)

// formatModel turns a raw model identifier into a short display label, mirroring
// entire.io's frontend formatModel (frontend/src/lib/model.ts) so the CLI's
// session list reads identically to the web Overview page.
// Examples:
// 
// 	"claude-opus-4-6"          -> "Opus 4.6"
// 	"claude-sonnet-4-20250514" -> "Sonnet 4"
// 	"gpt-4o"                   -> "GPT-4o"
// 	"gemini-2.0-flash"         -> "Gemini 2.0 Flash"
// 
// Unknown formats pass through unchanged. Empty/whitespace input returns ""
// (the web returns null; the CLI renders nothing for it).
func formatModel(model string) string {
	trimmed := strings.TrimSpace(model)
	if trimmed == "" {
		return ""
	}

// Claude: title-case family, then major[.minor], dropping any date suffix.
	if m := claudeModelRe.FindStringSubmatch(trimmed); m != nil {
		family, major, rest := m[1], m[2], m[3]
		familyLabel := strings.ToUpper(family[:1]) + strings.ToLower(family[1:])
		var minorParts []string
		for _, part := range strings.Split(rest, "-") {
			if part == "" {
				continue
			}
			// A 6+ digit chunk is a date suffix — ignore it and everything after.
			if dateSuffixRe.MatchString(part) {
				break
			}
			minorParts = append(minorParts, part)
		}
		if minor := strings.Join(minorParts, "."); minor != "" {
			return familyLabel + " " + major + "." + minor
		}
		return familyLabel + " " + major
	}

// GPT: gpt-4o -> "GPT-4o" (case-insensitive prefix; "gpt-" is 4 ASCII bytes).
	if len(trimmed) >= 4 && strings.EqualFold(trimmed[:4], "gpt-") {
		return "GPT-" + trimmed[4:]
	}

// Gemini: gemini-2.0-flash -> "Gemini 2.0 Flash" (upper-first each part).
	if strings.HasPrefix(strings.ToLower(trimmed), "gemini-") {
		parts := strings.Split(trimmed, "-")
		for i, part := range parts {
			parts[i] = upperFirst(part)
		}
		return strings.Join(parts, " ")
	}

return trimmed
}

// upperFirst upper-cases the first rune of s, leaving the remainder unchanged
// (matching the web's `part.charAt(0).toUpperCase() + part.slice(1)`).
func upperFirst(s string) string {
	if s == "" {
		return s
	}
	return strings.ToUpper(string([]rune(s)[0])) + string([]rune(s)[1:])
}
