Skip to content
Open
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
145 changes: 100 additions & 45 deletions cmd/entire/cli/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,33 +71,37 @@ func runStatus(w io.Writer, detailed bool) error {
return nil
}

sty := newStatusStyles(w)

if detailed {
return runStatusDetailed(w, settingsPath, localSettingsPath, projectExists, localExists)
return runStatusDetailed(w, sty, settingsPath, localSettingsPath, projectExists, localExists)
}

// Short output: just show the effective/merged state
settings, err := LoadEntireSettings()
s, err := LoadEntireSettings()
if err != nil {
return fmt.Errorf("failed to load settings: %w", err)
}

fmt.Fprintln(w, formatSettingsStatusShort(settings))
fmt.Fprintln(w)
fmt.Fprintln(w, formatSettingsStatusShort(s, sty))

if settings.Enabled {
writeActiveSessions(w)
if s.Enabled {
writeActiveSessions(w, sty)
}

return nil
}

// runStatusDetailed shows the effective status plus detailed status for each settings file.
func runStatusDetailed(w io.Writer, settingsPath, localSettingsPath string, projectExists, localExists bool) error {
func runStatusDetailed(w io.Writer, sty statusStyles, settingsPath, localSettingsPath string, projectExists, localExists bool) error {
// First show the effective/merged status
effectiveSettings, err := LoadEntireSettings()
if err != nil {
return fmt.Errorf("failed to load settings: %w", err)
}
fmt.Fprintln(w, formatSettingsStatusShort(effectiveSettings))
fmt.Fprintln(w)
fmt.Fprintln(w, formatSettingsStatusShort(effectiveSettings, sty))
fmt.Fprintln(w) // blank line

// Show project settings if it exists
Expand All @@ -106,7 +110,7 @@ func runStatusDetailed(w io.Writer, settingsPath, localSettingsPath string, proj
if err != nil {
return fmt.Errorf("failed to load project settings: %w", err)
}
fmt.Fprintln(w, formatSettingsStatus("Project", projectSettings))
fmt.Fprintln(w, formatSettingsStatus("Project", projectSettings, sty))
}

// Show local settings if it exists
Expand All @@ -115,42 +119,73 @@ func runStatusDetailed(w io.Writer, settingsPath, localSettingsPath string, proj
if err != nil {
return fmt.Errorf("failed to load local settings: %w", err)
}
fmt.Fprintln(w, formatSettingsStatus("Local", localSettings))
fmt.Fprintln(w, formatSettingsStatus("Local", localSettings, sty))
}

if effectiveSettings.Enabled {
writeActiveSessions(w)
writeActiveSessions(w, sty)
}

return nil
}

// formatSettingsStatusShort formats a short settings status line.
// Output format: "Enabled (manual-commit)" or "Disabled (auto-commit)"
func formatSettingsStatusShort(settings *EntireSettings) string {
displayName := settings.Strategy
if dn, ok := strategyInternalToDisplay[settings.Strategy]; ok {
// Output format: "Enabled · manual-commit · branch main" or "Disabled · auto-commit"
func formatSettingsStatusShort(s *EntireSettings, sty statusStyles) string {
displayName := s.Strategy
if dn, ok := strategyInternalToDisplay[s.Strategy]; ok {
displayName = dn
}

if settings.Enabled {
return fmt.Sprintf("Enabled (%s)", displayName)
var b strings.Builder

if s.Enabled {
b.WriteString(sty.render(sty.green, "●"))
b.WriteString(" ")
b.WriteString(sty.render(sty.bold, "Enabled"))
} else {
b.WriteString(sty.render(sty.red, "○"))
b.WriteString(" ")
b.WriteString(sty.render(sty.bold, "Disabled"))
}

b.WriteString(sty.render(sty.dim, " · "))
b.WriteString(displayName)

// Resolve branch from repo root
if repoRoot, err := paths.RepoRoot(); err == nil {
if branch := resolveWorktreeBranch(repoRoot); branch != "" {
b.WriteString(sty.render(sty.dim, " · "))
b.WriteString("branch ")
b.WriteString(sty.render(sty.cyan, branch))
}
}
return fmt.Sprintf("Disabled (%s)", displayName)

return b.String()
}

// formatSettingsStatus formats a settings status line with source prefix.
// Output format: "Project, enabled (manual-commit)" or "Local, disabled (auto-commit)"
func formatSettingsStatus(prefix string, settings *EntireSettings) string {
displayName := settings.Strategy
if dn, ok := strategyInternalToDisplay[settings.Strategy]; ok {
// Output format: "Project · enabled · manual-commit" or "Local · disabled · auto-commit"
func formatSettingsStatus(prefix string, s *EntireSettings, sty statusStyles) string {
displayName := s.Strategy
if dn, ok := strategyInternalToDisplay[s.Strategy]; ok {
displayName = dn
}

if settings.Enabled {
return fmt.Sprintf("%s, enabled (%s)", prefix, displayName)
var b strings.Builder
b.WriteString(sty.render(sty.bold, prefix))
b.WriteString(sty.render(sty.dim, " · "))

if s.Enabled {
b.WriteString("enabled")
} else {
b.WriteString("disabled")
}
return fmt.Sprintf("%s, disabled (%s)", prefix, displayName)

b.WriteString(sty.render(sty.dim, " · "))
b.WriteString(displayName)

return b.String()
}

// timeAgo formats a time as a human-readable relative duration.
Expand Down Expand Up @@ -181,7 +216,7 @@ type worktreeGroup struct {
const unknownPlaceholder = "(unknown)"

// writeActiveSessions writes active session information grouped by worktree.
func writeActiveSessions(w io.Writer) {
func writeActiveSessions(w io.Writer, sty statusStyles) {
store, err := session.NewStateStore()
if err != nil {
return
Expand Down Expand Up @@ -241,16 +276,21 @@ func writeActiveSessions(w io.Writer) {
})
}

// Track aggregate totals
var totalSessions int

fmt.Fprintln(w)
fmt.Fprintln(w, "Active Sessions:")
for i, g := range sortedGroups {
header := g.path
if g.branch != "" {
header += " (" + g.branch + ")"
printedHeader := false
for _, g := range sortedGroups {
if !printedHeader {
fmt.Fprintln(w, sty.sectionRule("Active Sessions", sty.width))
fmt.Fprintln(w)
printedHeader = true
}
fmt.Fprintf(w, " %s\n", header)

for _, st := range g.sessions {
totalSessions++

agentLabel := string(st.AgentType)
if agentLabel == "" {
agentLabel = unknownPlaceholder
Expand All @@ -261,29 +301,44 @@ func writeActiveSessions(w io.Writer) {
shortID = shortID[:7]
}

age := "started " + timeAgo(st.StartedAt)
// Line 1: Agent · shortID
fmt.Fprintf(w, "%s %s %s\n",
sty.render(sty.agent, agentLabel),
sty.render(sty.dim, "·"),
shortID)

// Show "active X ago" when LastInteractionTime differs meaningfully from StartedAt
activeStr := ""
if st.LastInteractionTime != nil && st.LastInteractionTime.Sub(st.StartedAt) > time.Minute {
activeStr = ", active " + timeAgo(*st.LastInteractionTime)
// Line 2: > "first prompt" (chevron + quoted, truncated)
if st.FirstPrompt != "" {
prompt := stringutil.TruncateRunes(st.FirstPrompt, 60, "...")
fmt.Fprintf(w, "%s \"%s\"\n", sty.render(sty.dim, ">"), prompt)
}

fmt.Fprintf(w, " [%s] %-9s %s%s\n",
agentLabel, shortID, age, activeStr)
// Line 3: stats line — started Xd ago · active now · files N · tokens X.Xk
var stats []string
stats = append(stats, "started "+timeAgo(st.StartedAt))

// Show first prompt on indented second line
if st.FirstPrompt != "" {
prompt := stringutil.TruncateRunes(st.FirstPrompt, 60, "...")
fmt.Fprintf(w, " \"%s\"\n", prompt)
if st.LastInteractionTime != nil && st.LastInteractionTime.Sub(st.StartedAt) > time.Minute {
stats = append(stats, activeTimeDisplay(st.LastInteractionTime))
}
}

// Blank line between groups, but not after the last one
if i < len(sortedGroups)-1 {
stats = append(stats, "tokens "+formatTokenCount(totalTokens(st.TokenUsage)))

statsLine := strings.Join(stats, sty.render(sty.dim, " · "))
fmt.Fprintln(w, sty.render(sty.dim, statsLine))
fmt.Fprintln(w)
}
}

// Footer: horizontal rule + session count
fmt.Fprintln(w, sty.horizontalRule(sty.width))
var footer string
if totalSessions == 1 {
footer = "1 session"
} else {
footer = fmt.Sprintf("%d sessions", totalSessions)
}
fmt.Fprintln(w, sty.render(sty.dim, footer))
fmt.Fprintln(w)
}

// resolveWorktreeBranch resolves the current branch for a worktree path.
Expand Down
155 changes: 155 additions & 0 deletions cmd/entire/cli/status_style.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cli

import (
"fmt"
"io"
"os"
"strconv"
"strings"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/entireio/cli/cmd/entire/cli/agent"

"golang.org/x/term"
)

// statusStyles holds pre-built lipgloss styles and terminal metadata.
type statusStyles struct {
colorEnabled bool
width int

// Styles
green lipgloss.Style
red lipgloss.Style
gray lipgloss.Style
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused gray field defined but never referenced

Low Severity

The gray field in statusStyles is defined and initialized but never read anywhere. Since statusStyles is a new unexported type introduced in this PR, all usage is visible in the diff — sty.green, sty.red, sty.bold, sty.dim, sty.agent, and sty.cyan are all referenced, but sty.gray is not. This is dead code.

Additional Locations (1)

Fix in Cursor Fix in Web

bold lipgloss.Style
dim lipgloss.Style
agent lipgloss.Style // amber/orange for agent names
cyan lipgloss.Style
}

// newStatusStyles creates styles appropriate for the output writer.
func newStatusStyles(w io.Writer) statusStyles {
useColor := shouldUseColor(w)
width := getTerminalWidth(w)

s := statusStyles{
colorEnabled: useColor,
width: width,
}

if useColor {
s.green = lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
s.red = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
s.gray = lipgloss.NewStyle().Foreground(lipgloss.Color("8"))
s.bold = lipgloss.NewStyle().Bold(true)
s.dim = lipgloss.NewStyle().Faint(true)
s.agent = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("214"))
s.cyan = lipgloss.NewStyle().Foreground(lipgloss.Color("6"))
}

return s
}

// render applies a style to text only when color is enabled.
func (s statusStyles) render(style lipgloss.Style, text string) string {
if !s.colorEnabled {
return text
}
return style.Render(text)
}

// shouldUseColor returns true if the writer supports color output.
func shouldUseColor(w io.Writer) bool {
if os.Getenv("NO_COLOR") != "" {
return false
}
if f, ok := w.(*os.File); ok {
return term.IsTerminal(int(f.Fd()))
}
return false
}

// getTerminalWidth returns the terminal width, capped at 80 with a fallback of 60.
// It first checks the writer itself, then falls back to Stdout/Stderr.
func getTerminalWidth(w io.Writer) int {
// Try the output writer first
if f, ok := w.(*os.File); ok {
if width, _, err := term.GetSize(int(f.Fd())); err == nil && width > 0 {
return min(width, 80)
}
}

// Fall back to Stdout, then Stderr
for _, f := range []*os.File{os.Stdout, os.Stderr} {
if f == nil {
continue
}
if width, _, err := term.GetSize(int(f.Fd())); err == nil && width > 0 {
return min(width, 80)
}
}

return 60
}

// formatTokenCount formats a token count for display.
// 0 → "0", 500 → "500", 1200 → "1.2k", 14300 → "14.3k"
func formatTokenCount(n int) string {
if n < 1000 {
return strconv.Itoa(n)
}
f := float64(n) / 1000.0
s := fmt.Sprintf("%.1f", f)
// Remove trailing ".0" for clean display (e.g., 1000 → "1k" not "1.0k")
s = strings.TrimSuffix(s, ".0")
return s + "k"
}

// totalTokens recursively sums all token fields including subagent tokens.
func totalTokens(tu *agent.TokenUsage) int {
if tu == nil {
return 0
}
total := tu.InputTokens + tu.CacheCreationTokens + tu.CacheReadTokens + tu.OutputTokens
total += totalTokens(tu.SubagentTokens)
return total
}

// horizontalRule renders a dimmed horizontal rule of the given width.
func (s statusStyles) horizontalRule(width int) string {
rule := strings.Repeat("─", width)
return s.render(s.dim, rule)
}

// sectionRule renders a section header like: ── Active Sessions ────────────
func (s statusStyles) sectionRule(label string, width int) string {
prefix := "── "
content := label + " "
usedWidth := len([]rune(prefix)) + len([]rune(content))
trailing := width - usedWidth
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Byte length used instead of display width for Unicode

Low Severity

sectionRule computes usedWidth via len(prefix) which returns the byte count, not the display column width. The prefix "── " contains two characters (U+2500, 3 bytes each in UTF-8), so len() returns 7 instead of the correct display width of 3. This makes the trailing rule 4 columns shorter than the terminal width.

Fix in Cursor Fix in Web

if trailing < 1 {
trailing = 1
}

var b strings.Builder
b.WriteString(s.render(s.dim, "── "))
b.WriteString(s.render(s.dim, label))
b.WriteString(" ")
b.WriteString(s.render(s.dim, strings.Repeat("─", trailing)))
return b.String()
}

// activeTimeDisplay formats a last interaction time for display.
// Returns "active now" for recent activity (<1min), otherwise "active Xm ago".
func activeTimeDisplay(lastInteraction *time.Time) string {
if lastInteraction == nil {
return ""
}
d := time.Since(*lastInteraction)
if d < time.Minute {
return "active now"
}
return "active " + timeAgo(*lastInteraction)
}
Loading