feat(tui): apply visual polish to dashboard tabs
Update all tab renderers to leverage the expanded theme palette and polished components: tab_overview.go: - Use PanelCard (accent-bordered variant) for daily token chart - Multi-color model bars: BlueBright, Cyan, Magenta, Yellow, Green for visual distinction between models - Pre-compute styles outside loops for better performance - Use Cyan for today's hourly chart, Magenta for last-hour chart tab_breakdown.go: - Apply consistent background styling - Use new accent variants for visual hierarchy tab_costs.go: - Proper background fill on cost tables - Accent coloring for cost highlights tab_sessions.go: - Background continuity in session list - Visual improvements to session detail view tab_settings.go: - Consistent styling with other tabs The result is a dashboard where each tab feels visually cohesive, with color providing semantic meaning (different colors for different models, metrics, and states) rather than arbitrary decoration. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,18 @@ func (a App) renderModelsTab(cw int) string {
|
|||||||
nameW = 14
|
nameW = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface).Bold(true)
|
||||||
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
|
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
|
costStyle := lipgloss.NewStyle().Foreground(t.GreenBright).Background(t.Surface)
|
||||||
|
shareStyle := lipgloss.NewStyle().Foreground(t.Cyan).Background(t.Surface)
|
||||||
|
|
||||||
|
// Model colors for visual interest - pre-compute styles to avoid allocation in loops
|
||||||
|
modelColors := []lipgloss.Color{t.BlueBright, t.Cyan, t.Magenta, t.Yellow, t.Green}
|
||||||
|
nameStyles := make([]lipgloss.Style, len(modelColors))
|
||||||
|
for i, color := range modelColors {
|
||||||
|
nameStyles[i] = lipgloss.NewStyle().Foreground(color).Background(t.Surface)
|
||||||
|
}
|
||||||
|
|
||||||
var tableBody strings.Builder
|
var tableBody strings.Builder
|
||||||
if a.isCompactLayout() {
|
if a.isCompactLayout() {
|
||||||
@@ -37,29 +47,30 @@ func (a App) renderModelsTab(cw int) string {
|
|||||||
}
|
}
|
||||||
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %8s %10s %6s", nameW, "Model", "Calls", "Cost", "Share")))
|
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %8s %10s %6s", nameW, "Model", "Calls", "Cost", "Share")))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
|
tableBody.WriteString(mutedStyle.Render(strings.Repeat("─", nameW+shareW+costW+callW+3)))
|
||||||
|
tableBody.WriteString("\n")
|
||||||
|
|
||||||
for _, ms := range models {
|
for i, ms := range models {
|
||||||
tableBody.WriteString(rowStyle.Render(fmt.Sprintf("%-*s %8s %10s %5.1f%%",
|
tableBody.WriteString(nameStyles[i%len(modelColors)].Render(fmt.Sprintf("%-*s", nameW, truncStr(shortModel(ms.Model), nameW))))
|
||||||
nameW,
|
tableBody.WriteString(rowStyle.Render(fmt.Sprintf(" %8s", cli.FormatNumber(int64(ms.APICalls)))))
|
||||||
truncStr(shortModel(ms.Model), nameW),
|
tableBody.WriteString(costStyle.Render(fmt.Sprintf(" %10s", cli.FormatCost(ms.EstimatedCost))))
|
||||||
cli.FormatNumber(int64(ms.APICalls)),
|
tableBody.WriteString(shareStyle.Render(fmt.Sprintf(" %5.1f%%", ms.SharePercent)))
|
||||||
cli.FormatCost(ms.EstimatedCost),
|
|
||||||
ms.SharePercent)))
|
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %8s %10s %10s %10s %6s", nameW, "Model", "Calls", "Input", "Output", "Cost", "Share")))
|
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %8s %10s %10s %10s %6s", nameW, "Model", "Calls", "Input", "Output", "Cost", "Share")))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
|
tableBody.WriteString(mutedStyle.Render(strings.Repeat("─", innerW)))
|
||||||
|
tableBody.WriteString("\n")
|
||||||
|
|
||||||
for _, ms := range models {
|
for i, ms := range models {
|
||||||
tableBody.WriteString(rowStyle.Render(fmt.Sprintf("%-*s %8s %10s %10s %10s %5.1f%%",
|
tableBody.WriteString(nameStyles[i%len(modelColors)].Render(fmt.Sprintf("%-*s", nameW, truncStr(shortModel(ms.Model), nameW))))
|
||||||
nameW,
|
tableBody.WriteString(rowStyle.Render(fmt.Sprintf(" %8s %10s %10s",
|
||||||
truncStr(shortModel(ms.Model), nameW),
|
|
||||||
cli.FormatNumber(int64(ms.APICalls)),
|
cli.FormatNumber(int64(ms.APICalls)),
|
||||||
cli.FormatTokens(ms.InputTokens),
|
cli.FormatTokens(ms.InputTokens),
|
||||||
cli.FormatTokens(ms.OutputTokens),
|
cli.FormatTokens(ms.OutputTokens))))
|
||||||
cli.FormatCost(ms.EstimatedCost),
|
tableBody.WriteString(costStyle.Render(fmt.Sprintf(" %10s", cli.FormatCost(ms.EstimatedCost))))
|
||||||
ms.SharePercent)))
|
tableBody.WriteString(shareStyle.Render(fmt.Sprintf(" %5.1f%%", ms.SharePercent)))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,8 +90,11 @@ func (a App) renderProjectsTab(cw int) string {
|
|||||||
nameW = 18
|
nameW = 18
|
||||||
}
|
}
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface).Bold(true)
|
||||||
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
|
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
|
nameStyle := lipgloss.NewStyle().Foreground(t.Cyan).Background(t.Surface)
|
||||||
|
costStyle := lipgloss.NewStyle().Foreground(t.GreenBright).Background(t.Surface)
|
||||||
|
|
||||||
var tableBody strings.Builder
|
var tableBody strings.Builder
|
||||||
if a.isCompactLayout() {
|
if a.isCompactLayout() {
|
||||||
@@ -92,27 +106,28 @@ func (a App) renderProjectsTab(cw int) string {
|
|||||||
}
|
}
|
||||||
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %6s %10s", nameW, "Project", "Sess.", "Cost")))
|
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %6s %10s", nameW, "Project", "Sess.", "Cost")))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
|
tableBody.WriteString(mutedStyle.Render(strings.Repeat("─", nameW+costW+sessW+2)))
|
||||||
|
tableBody.WriteString("\n")
|
||||||
|
|
||||||
for _, ps := range projects {
|
for _, ps := range projects {
|
||||||
tableBody.WriteString(rowStyle.Render(fmt.Sprintf("%-*s %6d %10s",
|
tableBody.WriteString(nameStyle.Render(fmt.Sprintf("%-*s", nameW, truncStr(ps.Project, nameW))))
|
||||||
nameW,
|
tableBody.WriteString(rowStyle.Render(fmt.Sprintf(" %6d", ps.Sessions)))
|
||||||
truncStr(ps.Project, nameW),
|
tableBody.WriteString(costStyle.Render(fmt.Sprintf(" %10s", cli.FormatCost(ps.EstimatedCost))))
|
||||||
ps.Sessions,
|
|
||||||
cli.FormatCost(ps.EstimatedCost))))
|
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %6s %8s %10s %10s", nameW, "Project", "Sess.", "Prompts", "Tokens", "Cost")))
|
tableBody.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %6s %8s %10s %10s", nameW, "Project", "Sess.", "Prompts", "Tokens", "Cost")))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
|
tableBody.WriteString(mutedStyle.Render(strings.Repeat("─", innerW)))
|
||||||
|
tableBody.WriteString("\n")
|
||||||
|
|
||||||
for _, ps := range projects {
|
for _, ps := range projects {
|
||||||
tableBody.WriteString(rowStyle.Render(fmt.Sprintf("%-*s %6d %8s %10s %10s",
|
tableBody.WriteString(nameStyle.Render(fmt.Sprintf("%-*s", nameW, truncStr(ps.Project, nameW))))
|
||||||
nameW,
|
tableBody.WriteString(rowStyle.Render(fmt.Sprintf(" %6d %8s %10s",
|
||||||
truncStr(ps.Project, nameW),
|
|
||||||
ps.Sessions,
|
ps.Sessions,
|
||||||
cli.FormatNumber(int64(ps.Prompts)),
|
cli.FormatNumber(int64(ps.Prompts)),
|
||||||
cli.FormatTokens(ps.TotalTokens),
|
cli.FormatTokens(ps.TotalTokens))))
|
||||||
cli.FormatCost(ps.EstimatedCost))))
|
tableBody.WriteString(costStyle.Render(fmt.Sprintf(" %10s", cli.FormatCost(ps.EstimatedCost))))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,14 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
nameW = 14
|
nameW = 14
|
||||||
}
|
}
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface).Bold(true)
|
||||||
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
costValueStyle := lipgloss.NewStyle().Foreground(t.GreenBright).Background(t.Surface)
|
||||||
|
modelNameStyle := lipgloss.NewStyle().Foreground(t.BlueBright).Background(t.Surface)
|
||||||
|
tokenCostStyle := lipgloss.NewStyle().Foreground(t.Cyan).Background(t.Surface)
|
||||||
|
spaceStyle := lipgloss.NewStyle().Background(t.Surface)
|
||||||
|
|
||||||
var tableBody strings.Builder
|
var tableBody strings.Builder
|
||||||
if a.isCompactLayout() {
|
if a.isCompactLayout() {
|
||||||
@@ -70,10 +73,8 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
|
|
||||||
for _, mc := range modelCosts {
|
for _, mc := range modelCosts {
|
||||||
tableBody.WriteString(rowStyle.Render(fmt.Sprintf("%-*s %10s",
|
tableBody.WriteString(modelNameStyle.Render(fmt.Sprintf("%-*s", nameW, truncStr(shortModel(mc.Model), nameW))))
|
||||||
nameW,
|
tableBody.WriteString(costValueStyle.Render(fmt.Sprintf(" %10s", cli.FormatCost(mc.TotalCost))))
|
||||||
truncStr(shortModel(mc.Model), nameW),
|
|
||||||
cli.FormatCost(mc.TotalCost))))
|
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
tableBody.WriteString(mutedStyle.Render(strings.Repeat("─", nameW+totalW+1)))
|
tableBody.WriteString(mutedStyle.Render(strings.Repeat("─", nameW+totalW+1)))
|
||||||
@@ -84,13 +85,12 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
|
|
||||||
for _, mc := range modelCosts {
|
for _, mc := range modelCosts {
|
||||||
tableBody.WriteString(rowStyle.Render(fmt.Sprintf("%-*s %10s %10s %10s %10s",
|
tableBody.WriteString(modelNameStyle.Render(fmt.Sprintf("%-*s", nameW, truncStr(shortModel(mc.Model), nameW))))
|
||||||
nameW,
|
tableBody.WriteString(tokenCostStyle.Render(fmt.Sprintf(" %10s %10s %10s",
|
||||||
truncStr(shortModel(mc.Model), nameW),
|
|
||||||
cli.FormatCost(mc.InputCost),
|
cli.FormatCost(mc.InputCost),
|
||||||
cli.FormatCost(mc.OutputCost),
|
cli.FormatCost(mc.OutputCost),
|
||||||
cli.FormatCost(mc.CacheCost),
|
cli.FormatCost(mc.CacheCost))))
|
||||||
cli.FormatCost(mc.TotalCost))))
|
tableBody.WriteString(costValueStyle.Render(fmt.Sprintf(" %10s", cli.FormatCost(mc.TotalCost))))
|
||||||
tableBody.WriteString("\n")
|
tableBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +126,16 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
|
|
||||||
var body strings.Builder
|
var body strings.Builder
|
||||||
body.WriteString(bar.ViewAs(pct))
|
body.WriteString(bar.ViewAs(pct))
|
||||||
fmt.Fprintf(&body, " %.0f%%\n", pct*100)
|
body.WriteString(spaceStyle.Render(" "))
|
||||||
fmt.Fprintf(&body, "%s %s / %s %s",
|
body.WriteString(valueStyle.Render(fmt.Sprintf("%.0f%%", pct*100)))
|
||||||
labelStyle.Render("Used"),
|
body.WriteString("\n")
|
||||||
valueStyle.Render(fmt.Sprintf("$%.2f", ol.UsedCredits)),
|
body.WriteString(labelStyle.Render("Used"))
|
||||||
valueStyle.Render(fmt.Sprintf("$%.2f", ol.MonthlyCreditLimit)),
|
body.WriteString(spaceStyle.Render(" "))
|
||||||
labelStyle.Render(ol.Currency))
|
body.WriteString(valueStyle.Render(fmt.Sprintf("$%.2f", ol.UsedCredits)))
|
||||||
|
body.WriteString(spaceStyle.Render(" / "))
|
||||||
|
body.WriteString(valueStyle.Render(fmt.Sprintf("$%.2f", ol.MonthlyCreditLimit)))
|
||||||
|
body.WriteString(spaceStyle.Render(" "))
|
||||||
|
body.WriteString(labelStyle.Render(ol.Currency))
|
||||||
|
|
||||||
progressCard = components.ContentCard("Overage Spend", body.String(), halves[0])
|
progressCard = components.ContentCard("Overage Spend", body.String(), halves[0])
|
||||||
} else {
|
} else {
|
||||||
@@ -159,12 +163,14 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
return topDays[i].Date.After(topDays[j].Date)
|
return topDays[i].Date.After(topDays[j].Date)
|
||||||
})
|
})
|
||||||
for _, d := range topDays {
|
for _, d := range topDays {
|
||||||
fmt.Fprintf(&spendBody, "%s %s\n",
|
spendBody.WriteString(valueStyle.Render(d.Date.Format("Jan 02")))
|
||||||
valueStyle.Render(d.Date.Format("Jan 02")),
|
spendBody.WriteString(spaceStyle.Render(" "))
|
||||||
lipgloss.NewStyle().Foreground(t.Green).Render(cli.FormatCost(d.EstimatedCost)))
|
spendBody.WriteString(lipgloss.NewStyle().Foreground(t.Green).Background(t.Surface).Render(cli.FormatCost(d.EstimatedCost)))
|
||||||
|
spendBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
spendBody.WriteString("No data\n")
|
spendBody.WriteString(labelStyle.Render("No data"))
|
||||||
|
spendBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
spendCard := components.ContentCard("Top Spend Days", spendBody.String(), halves[1])
|
spendCard := components.ContentCard("Top Spend Days", spendBody.String(), halves[1])
|
||||||
|
|
||||||
@@ -189,16 +195,21 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
promptsPerSess = float64(stats.TotalPrompts) / float64(stats.TotalSessions)
|
promptsPerSess = float64(stats.TotalPrompts) / float64(stats.TotalSessions)
|
||||||
}
|
}
|
||||||
|
|
||||||
effMetrics := []struct{ name, value string }{
|
effMetrics := []struct {
|
||||||
{"Tokens/Prompt", cli.FormatTokens(tokPerPrompt)},
|
name string
|
||||||
{"Output/Prompt", cli.FormatTokens(outPerPrompt)},
|
value string
|
||||||
{"Prompts/Session", fmt.Sprintf("%.1f", promptsPerSess)},
|
color lipgloss.Color
|
||||||
{"Minutes/Day", fmt.Sprintf("%.0f", stats.MinutesPerDay)},
|
}{
|
||||||
|
{"Tokens/Prompt", cli.FormatTokens(tokPerPrompt), t.Cyan},
|
||||||
|
{"Output/Prompt", cli.FormatTokens(outPerPrompt), t.Cyan},
|
||||||
|
{"Prompts/Session", fmt.Sprintf("%.1f", promptsPerSess), t.Magenta},
|
||||||
|
{"Minutes/Day", fmt.Sprintf("%.0f", stats.MinutesPerDay), t.Yellow},
|
||||||
}
|
}
|
||||||
|
|
||||||
var effBody strings.Builder
|
var effBody strings.Builder
|
||||||
for _, m := range effMetrics {
|
for _, m := range effMetrics {
|
||||||
effBody.WriteString(rowStyle.Render(fmt.Sprintf("%-20s %10s", m.name, m.value)))
|
effBody.WriteString(labelStyle.Render(fmt.Sprintf("%-20s", m.name)))
|
||||||
|
effBody.WriteString(lipgloss.NewStyle().Foreground(m.color).Background(t.Surface).Render(fmt.Sprintf(" %10s", m.value)))
|
||||||
effBody.WriteString("\n")
|
effBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,11 +221,11 @@ func (a App) renderCostsTab(cw int) string {
|
|||||||
// renderSubscriptionCard renders the rate limit + overage card at the top of the costs tab.
|
// renderSubscriptionCard renders the rate limit + overage card at the top of the costs tab.
|
||||||
func (a App) renderSubscriptionCard(cw int) string {
|
func (a App) renderSubscriptionCard(cw int) string {
|
||||||
t := theme.Active
|
t := theme.Active
|
||||||
hintStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
hintStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
|
||||||
|
|
||||||
// No session key configured
|
// No session key configured
|
||||||
if a.subData == nil && !a.subFetching {
|
if a.subData == nil && !a.subFetching {
|
||||||
cfg, _ := config.Load()
|
cfg := loadConfigOrDefault()
|
||||||
if config.GetSessionKey(cfg) == "" {
|
if config.GetSessionKey(cfg) == "" {
|
||||||
return components.ContentCard("Subscription",
|
return components.ContentCard("Subscription",
|
||||||
hintStyle.Render("Configure session key in Settings to see rate limits"),
|
hintStyle.Render("Configure session key in Settings to see rate limits"),
|
||||||
@@ -235,7 +246,7 @@ func (a App) renderSubscriptionCard(cw int) string {
|
|||||||
|
|
||||||
// Error with no usable data
|
// Error with no usable data
|
||||||
if a.subData.Usage == nil && a.subData.Error != nil {
|
if a.subData.Usage == nil && a.subData.Error != nil {
|
||||||
warnStyle := lipgloss.NewStyle().Foreground(t.Orange)
|
warnStyle := lipgloss.NewStyle().Foreground(t.Orange).Background(t.Surface)
|
||||||
return components.ContentCard("Subscription",
|
return components.ContentCard("Subscription",
|
||||||
warnStyle.Render(fmt.Sprintf("Error: %s", a.subData.Error)),
|
warnStyle.Render(fmt.Sprintf("Error: %s", a.subData.Error)),
|
||||||
cw) + "\n"
|
cw) + "\n"
|
||||||
@@ -285,12 +296,12 @@ func (a App) renderSubscriptionCard(cw int) string {
|
|||||||
if ol := a.subData.Overage; ol != nil && ol.IsEnabled && ol.MonthlyCreditLimit > 0 {
|
if ol := a.subData.Overage; ol != nil && ol.IsEnabled && ol.MonthlyCreditLimit > 0 {
|
||||||
pct := ol.UsedCredits / ol.MonthlyCreditLimit
|
pct := ol.UsedCredits / ol.MonthlyCreditLimit
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(lipgloss.NewStyle().Foreground(t.TextDim).Render(strings.Repeat("─", innerW)))
|
body.WriteString(lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface).Render(strings.Repeat("─", innerW)))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(components.RateLimitBar("Overage",
|
body.WriteString(components.RateLimitBar("Overage",
|
||||||
pct, time.Time{}, labelW, barW))
|
pct, time.Time{}, labelW, barW))
|
||||||
|
|
||||||
spendStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
spendStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
|
||||||
body.WriteString(spendStyle.Render(
|
body.WriteString(spendStyle.Render(
|
||||||
fmt.Sprintf(" $%.2f / $%.2f", ol.UsedCredits, ol.MonthlyCreditLimit)))
|
fmt.Sprintf(" $%.2f / $%.2f", ol.UsedCredits, ol.MonthlyCreditLimit)))
|
||||||
}
|
}
|
||||||
@@ -298,7 +309,7 @@ func (a App) renderSubscriptionCard(cw int) string {
|
|||||||
// Fetch timestamp
|
// Fetch timestamp
|
||||||
if !a.subData.FetchedAt.IsZero() {
|
if !a.subData.FetchedAt.IsZero() {
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
tsStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
tsStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
|
||||||
body.WriteString(tsStyle.Render("Updated " + a.subData.FetchedAt.Format("3:04 PM")))
|
body.WriteString(tsStyle.Render("Updated " + a.subData.FetchedAt.Format("3:04 PM")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
models := a.models
|
models := a.models
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
// Row 1: Metric cards
|
// Row 1: Metric cards with colored values
|
||||||
costDelta := ""
|
costDelta := ""
|
||||||
if prev.CostPerDay > 0 {
|
if prev.CostPerDay > 0 {
|
||||||
costDelta = fmt.Sprintf("%s/day (%s)", cli.FormatCost(stats.CostPerDay), cli.FormatDelta(stats.CostPerDay, prev.CostPerDay))
|
costDelta = fmt.Sprintf("%s/day (%s)", cli.FormatCost(stats.CostPerDay), cli.FormatDelta(stats.CostPerDay, prev.CostPerDay))
|
||||||
@@ -54,7 +54,7 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
b.WriteString(components.MetricCardRow(cards, cw))
|
b.WriteString(components.MetricCardRow(cards, cw))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
// Row 2: Daily token usage chart
|
// Row 2: Daily token usage chart - use PanelCard for emphasis
|
||||||
if len(days) > 0 {
|
if len(days) > 0 {
|
||||||
chartVals := make([]float64, len(days))
|
chartVals := make([]float64, len(days))
|
||||||
chartLabels := chartDateLabels(days)
|
chartLabels := chartDateLabels(days)
|
||||||
@@ -62,9 +62,9 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
chartVals[len(days)-1-i] = float64(d.InputTokens + d.OutputTokens + d.CacheCreation5m + d.CacheCreation1h)
|
chartVals[len(days)-1-i] = float64(d.InputTokens + d.OutputTokens + d.CacheCreation5m + d.CacheCreation1h)
|
||||||
}
|
}
|
||||||
chartInnerW := components.CardInnerWidth(cw)
|
chartInnerW := components.CardInnerWidth(cw)
|
||||||
b.WriteString(components.ContentCard(
|
b.WriteString(components.PanelCard(
|
||||||
fmt.Sprintf("Daily Token Usage (%dd)", a.days),
|
fmt.Sprintf("Daily Token Usage (%dd)", a.days),
|
||||||
components.BarChart(chartVals, chartLabels, t.Blue, chartInnerW, 10),
|
components.BarChart(chartVals, chartLabels, t.BlueBright, chartInnerW, 10),
|
||||||
cw,
|
cw,
|
||||||
))
|
))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -88,7 +88,7 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
}
|
}
|
||||||
todayCard = components.ContentCard(
|
todayCard = components.ContentCard(
|
||||||
fmt.Sprintf("Today (%s)", cli.FormatTokens(todayTotal)),
|
fmt.Sprintf("Today (%s)", cli.FormatTokens(todayTotal)),
|
||||||
components.BarChart(hourVals, hourLabels24(), t.Blue, components.CardInnerWidth(liveHalves[0]), liveChartH),
|
components.BarChart(hourVals, hourLabels24(), t.Cyan, components.CardInnerWidth(liveHalves[0]), liveChartH),
|
||||||
liveHalves[0],
|
liveHalves[0],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
}
|
}
|
||||||
lastHourCard = components.ContentCard(
|
lastHourCard = components.ContentCard(
|
||||||
fmt.Sprintf("Last Hour (%s)", cli.FormatTokens(hourTotal)),
|
fmt.Sprintf("Last Hour (%s)", cli.FormatTokens(hourTotal)),
|
||||||
components.BarChart(minVals, minuteLabels(), t.Accent, components.CardInnerWidth(liveHalves[1]), liveChartH),
|
components.BarChart(minVals, minuteLabels(), t.Magenta, components.CardInnerWidth(liveHalves[1]), liveChartH),
|
||||||
liveHalves[1],
|
liveHalves[1],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -127,10 +127,7 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
halves := components.LayoutRow(cw, 2)
|
halves := components.LayoutRow(cw, 2)
|
||||||
innerW := components.CardInnerWidth(halves[0])
|
innerW := components.CardInnerWidth(halves[0])
|
||||||
|
|
||||||
nameStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
// Model split with colored bars per model
|
||||||
barStyle := lipgloss.NewStyle().Foreground(t.Accent)
|
|
||||||
pctStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
|
||||||
|
|
||||||
var modelBody strings.Builder
|
var modelBody strings.Builder
|
||||||
limit := 5
|
limit := 5
|
||||||
if len(models) < limit {
|
if len(models) < limit {
|
||||||
@@ -150,18 +147,36 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
if barMaxLen < 1 {
|
if barMaxLen < 1 {
|
||||||
barMaxLen = 1
|
barMaxLen = 1
|
||||||
}
|
}
|
||||||
for _, ms := range models[:limit] {
|
|
||||||
|
// Color palette for models - pre-compute styles to avoid allocation in loop
|
||||||
|
modelColors := []lipgloss.Color{t.BlueBright, t.Cyan, t.Magenta, t.Yellow, t.Green}
|
||||||
|
sepStyle := lipgloss.NewStyle().Background(t.Surface)
|
||||||
|
nameStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
|
|
||||||
|
// Pre-compute bar and percent styles for each color
|
||||||
|
barStyles := make([]lipgloss.Style, len(modelColors))
|
||||||
|
pctStyles := make([]lipgloss.Style, len(modelColors))
|
||||||
|
for i, color := range modelColors {
|
||||||
|
barStyles[i] = lipgloss.NewStyle().Foreground(color).Background(t.Surface)
|
||||||
|
pctStyles[i] = lipgloss.NewStyle().Foreground(color).Background(t.Surface).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, ms := range models[:limit] {
|
||||||
barLen := 0
|
barLen := 0
|
||||||
if maxShare > 0 {
|
if maxShare > 0 {
|
||||||
barLen = int(ms.SharePercent / maxShare * float64(barMaxLen))
|
barLen = int(ms.SharePercent / maxShare * float64(barMaxLen))
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&modelBody, "%s %s %s\n",
|
|
||||||
nameStyle.Render(fmt.Sprintf("%-*s", nameW, shortModel(ms.Model))),
|
colorIdx := i % len(modelColors)
|
||||||
barStyle.Render(strings.Repeat("█", barLen)),
|
modelBody.WriteString(nameStyle.Render(fmt.Sprintf("%-*s", nameW, shortModel(ms.Model))))
|
||||||
pctStyle.Render(fmt.Sprintf("%.0f%%", ms.SharePercent)))
|
modelBody.WriteString(sepStyle.Render(" "))
|
||||||
|
modelBody.WriteString(barStyles[colorIdx].Render(strings.Repeat("█", barLen)))
|
||||||
|
modelBody.WriteString(sepStyle.Render(" "))
|
||||||
|
modelBody.WriteString(pctStyles[colorIdx].Render(fmt.Sprintf("%3.0f%%", ms.SharePercent)))
|
||||||
|
modelBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compact activity: aggregate prompts into 4-hour buckets
|
// Activity patterns with time-of-day coloring
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
since := now.AddDate(0, 0, -a.days)
|
since := now.AddDate(0, 0, -a.days)
|
||||||
hours := pipeline.AggregateHourly(a.filtered, since, now)
|
hours := pipeline.AggregateHourly(a.filtered, since, now)
|
||||||
@@ -172,11 +187,11 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
color lipgloss.Color
|
color lipgloss.Color
|
||||||
}
|
}
|
||||||
buckets := []actBucket{
|
buckets := []actBucket{
|
||||||
{"Night 00-03", 0, t.Red},
|
{"Night 00-03", 0, t.Magenta},
|
||||||
{"Early 04-07", 0, t.Yellow},
|
{"Early 04-07", 0, t.Orange},
|
||||||
{"Morning 08-11", 0, t.Green},
|
{"Morning 08-11", 0, t.GreenBright},
|
||||||
{"Midday 12-15", 0, t.Green},
|
{"Midday 12-15", 0, t.Green},
|
||||||
{"Evening 16-19", 0, t.Green},
|
{"Evening 16-19", 0, t.Cyan},
|
||||||
{"Late 20-23", 0, t.Yellow},
|
{"Late 20-23", 0, t.Yellow},
|
||||||
}
|
}
|
||||||
for _, h := range hours {
|
for _, h := range hours {
|
||||||
@@ -196,31 +211,34 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
|
|
||||||
actInnerW := components.CardInnerWidth(halves[1])
|
actInnerW := components.CardInnerWidth(halves[1])
|
||||||
|
|
||||||
// Compute number column width from actual data so bars never overflow.
|
// Compute number column width
|
||||||
maxNumW := 5
|
maxNumW := 5
|
||||||
for _, bk := range buckets {
|
for _, bk := range buckets {
|
||||||
if nw := len(cli.FormatNumber(int64(bk.total))); nw > maxNumW {
|
if nw := len(cli.FormatNumber(int64(bk.total))); nw > maxNumW {
|
||||||
maxNumW = nw
|
maxNumW = nw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// prefix = 13 (label) + 1 (space) + maxNumW (number) + 1 (space)
|
|
||||||
actBarMax := actInnerW - 15 - maxNumW
|
actBarMax := actInnerW - 15 - maxNumW
|
||||||
if actBarMax < 1 {
|
if actBarMax < 1 {
|
||||||
actBarMax = 1
|
actBarMax = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
numStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
|
numStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
|
|
||||||
var actBody strings.Builder
|
var actBody strings.Builder
|
||||||
for _, bk := range buckets {
|
for _, bk := range buckets {
|
||||||
bl := 0
|
bl := 0
|
||||||
if maxBucket > 0 {
|
if maxBucket > 0 {
|
||||||
bl = bk.total * actBarMax / maxBucket
|
bl = bk.total * actBarMax / maxBucket
|
||||||
}
|
}
|
||||||
bar := lipgloss.NewStyle().Foreground(bk.color).Render(strings.Repeat("█", bl))
|
barStyle := lipgloss.NewStyle().Foreground(bk.color).Background(t.Surface)
|
||||||
fmt.Fprintf(&actBody, "%s %s %s\n",
|
actBody.WriteString(labelStyle.Render(bk.label))
|
||||||
numStyle.Render(bk.label),
|
actBody.WriteString(sepStyle.Render(" "))
|
||||||
numStyle.Render(fmt.Sprintf("%*s", maxNumW, cli.FormatNumber(int64(bk.total)))),
|
actBody.WriteString(numStyle.Render(fmt.Sprintf("%*s", maxNumW, cli.FormatNumber(int64(bk.total)))))
|
||||||
bar)
|
actBody.WriteString(sepStyle.Render(" "))
|
||||||
|
actBody.WriteString(barStyle.Render(strings.Repeat("█", bl)))
|
||||||
|
actBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
modelCard := components.ContentCard("Model Split", modelBody.String(), halves[0])
|
modelCard := components.ContentCard("Model Split", modelBody.String(), halves[0])
|
||||||
@@ -236,7 +254,7 @@ func (a App) renderOverviewTab(cw int) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// hourLabels24 returns X-axis labels for 24 hourly buckets (one per hour).
|
// hourLabels24 returns X-axis labels for 24 hourly buckets.
|
||||||
func hourLabels24() []string {
|
func hourLabels24() []string {
|
||||||
labels := make([]string, 24)
|
labels := make([]string, 24)
|
||||||
for i := 0; i < 24; i++ {
|
for i := 0; i < 24; i++ {
|
||||||
@@ -253,8 +271,7 @@ func hourLabels24() []string {
|
|||||||
return labels
|
return labels
|
||||||
}
|
}
|
||||||
|
|
||||||
// minuteLabels returns X-axis labels for 12 five-minute buckets (one per bucket).
|
// minuteLabels returns X-axis labels for 12 five-minute buckets.
|
||||||
// Bucket 0 is oldest (55-60 min ago), bucket 11 is newest (0-5 min ago).
|
|
||||||
func minuteLabels() []string {
|
func minuteLabels() []string {
|
||||||
return []string{"-55", "-50", "-45", "-40", "-35", "-30", "-25", "-20", "-15", "-10", "-5", "now"}
|
return []string{"-55", "-50", "-45", "-40", "-35", "-30", "-25", "-20", "-15", "-10", "-5", "now"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ const (
|
|||||||
sessViewDetail // Full-screen detail
|
sessViewDetail // Full-screen detail
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Layout constants for sessions tab height calculations.
|
||||||
|
const (
|
||||||
|
sessListOverhead = 6 // card border (2) + header row (2) + footer hint (2)
|
||||||
|
sessDetailOverhead = 4 // card border (2) + title (1) + gap (1)
|
||||||
|
sessMinVisible = 5 // minimum visible rows in any pane
|
||||||
|
)
|
||||||
|
|
||||||
// sessionsState holds the sessions tab state.
|
// sessionsState holds the sessions tab state.
|
||||||
type sessionsState struct {
|
type sessionsState struct {
|
||||||
cursor int
|
cursor int
|
||||||
@@ -79,17 +86,21 @@ func (a App) renderSessionsContent(filtered []model.SessionStats, cw, h int) str
|
|||||||
// Show search input when in search mode
|
// Show search input when in search mode
|
||||||
if ss.searching {
|
if ss.searching {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
searchStyle := lipgloss.NewStyle().Foreground(t.Accent)
|
searchStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface).Bold(true)
|
||||||
|
spaceStyle := lipgloss.NewStyle().Background(t.Surface)
|
||||||
b.WriteString(searchStyle.Render(" Search: "))
|
b.WriteString(searchStyle.Render(" Search: "))
|
||||||
b.WriteString(ss.searchInput.View())
|
b.WriteString(ss.searchInput.View())
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
hintStyle := lipgloss.NewStyle().Foreground(t.TextDim)
|
hintStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
|
||||||
b.WriteString(hintStyle.Render(" [Enter] apply [Esc] cancel"))
|
keyStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface)
|
||||||
|
b.WriteString(spaceStyle.Render(" ") + hintStyle.Render("[") + keyStyle.Render("Enter") + hintStyle.Render("] apply [") +
|
||||||
|
keyStyle.Render("Esc") + hintStyle.Render("] cancel"))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
// Show preview of filtered results
|
// Show preview of filtered results
|
||||||
previewFiltered := filterSessionsBySearch(a.filtered, ss.searchInput.Value())
|
previewFiltered := filterSessionsBySearch(a.filtered, ss.searchInput.Value())
|
||||||
b.WriteString(hintStyle.Render(fmt.Sprintf(" %d sessions match", len(previewFiltered))))
|
countStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
|
b.WriteString(countStyle.Render(fmt.Sprintf(" %d sessions match", len(previewFiltered))))
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -102,10 +113,10 @@ func (a App) renderSessionsContent(filtered []model.SessionStats, cw, h int) str
|
|||||||
|
|
||||||
if len(filtered) == 0 {
|
if len(filtered) == 0 {
|
||||||
var body strings.Builder
|
var body strings.Builder
|
||||||
body.WriteString(lipgloss.NewStyle().Foreground(t.TextMuted).Render("No sessions found"))
|
body.WriteString(lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface).Render("No sessions found"))
|
||||||
if ss.searchQuery != "" {
|
if ss.searchQuery != "" {
|
||||||
body.WriteString("\n\n")
|
body.WriteString("\n\n")
|
||||||
body.WriteString(lipgloss.NewStyle().Foreground(t.TextDim).Render("[Esc] clear search [/] new search"))
|
body.WriteString(lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface).Render("[Esc] clear search [/] new search"))
|
||||||
}
|
}
|
||||||
return components.ContentCard(title, body.String(), cw)
|
return components.ContentCard(title, body.String(), cw)
|
||||||
}
|
}
|
||||||
@@ -157,15 +168,15 @@ func (a App) renderSessionsSplit(sessions []model.SessionStats, cw, h int) strin
|
|||||||
// Left pane: condensed session list
|
// Left pane: condensed session list
|
||||||
leftInner := components.CardInnerWidth(leftW)
|
leftInner := components.CardInnerWidth(leftW)
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
rowStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
selectedStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.SurfaceBright).Bold(true)
|
||||||
selectedStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface).Bold(true)
|
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
costStyle := lipgloss.NewStyle().Foreground(t.Green).Background(t.Surface)
|
||||||
|
|
||||||
var leftBody strings.Builder
|
var leftBody strings.Builder
|
||||||
visible := h - 6 // card border (2) + header row (2) + footer hint (2)
|
visible := h - sessListOverhead
|
||||||
if visible < 5 {
|
if visible < sessMinVisible {
|
||||||
visible = 5
|
visible = sessMinVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := ss.offset
|
offset := ss.offset
|
||||||
@@ -198,18 +209,22 @@ func (a App) renderSessionsSplit(sessions []model.SessionStats, cw, h int) strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if i == cursor {
|
if i == cursor {
|
||||||
fullLine := leftPart + strings.Repeat(" ", padN) + costStr
|
// Selected row with bright background and accent marker
|
||||||
// Pad to full width for continuous highlight background
|
selectedCostStyle := lipgloss.NewStyle().Foreground(t.GreenBright).Background(t.SurfaceBright).Bold(true)
|
||||||
if len(fullLine) < leftInner {
|
marker := lipgloss.NewStyle().Foreground(t.AccentBright).Background(t.SurfaceBright).Render("▸ ")
|
||||||
fullLine += strings.Repeat(" ", leftInner-len(fullLine))
|
leftBody.WriteString(marker + selectedStyle.Render(leftPart) +
|
||||||
}
|
lipgloss.NewStyle().Background(t.SurfaceBright).Render(strings.Repeat(" ", max(1, padN-2))) +
|
||||||
leftBody.WriteString(selectedStyle.Render(fullLine))
|
selectedCostStyle.Render(costStr) +
|
||||||
|
lipgloss.NewStyle().Background(t.SurfaceBright).Render(strings.Repeat(" ", max(0, leftInner-len(leftPart)-padN-len(costStr)))))
|
||||||
} else {
|
} else {
|
||||||
|
// Normal row
|
||||||
leftBody.WriteString(
|
leftBody.WriteString(
|
||||||
mutedStyle.Render(fmt.Sprintf("%-13s", startStr)) + " " +
|
lipgloss.NewStyle().Background(t.Surface).Render(" ") +
|
||||||
|
mutedStyle.Render(fmt.Sprintf("%-13s", startStr)) +
|
||||||
|
lipgloss.NewStyle().Background(t.Surface).Render(" ") +
|
||||||
rowStyle.Render(dur) +
|
rowStyle.Render(dur) +
|
||||||
strings.Repeat(" ", padN) +
|
lipgloss.NewStyle().Background(t.Surface).Render(strings.Repeat(" ", padN-2)) +
|
||||||
mutedStyle.Render(costStr))
|
costStyle.Render(costStr))
|
||||||
}
|
}
|
||||||
leftBody.WriteString("\n")
|
leftBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -223,10 +238,10 @@ func (a App) renderSessionsSplit(sessions []model.SessionStats, cw, h int) strin
|
|||||||
|
|
||||||
// Right pane: full session detail with scroll support
|
// Right pane: full session detail with scroll support
|
||||||
sel := sessions[cursor]
|
sel := sessions[cursor]
|
||||||
rightBody := a.renderDetailBody(sel, rightW, headerStyle, mutedStyle)
|
rightBody := a.renderDetailBody(sel, rightW, mutedStyle)
|
||||||
|
|
||||||
// Apply detail scroll offset
|
// Apply detail scroll offset
|
||||||
rightBody = a.applyDetailScroll(rightBody, h-4) // card border (2) + title (1) + gap (1)
|
rightBody = a.applyDetailScroll(rightBody, h-sessDetailOverhead)
|
||||||
|
|
||||||
titleStr := "Session " + shortID(sel.SessionID)
|
titleStr := "Session " + shortID(sel.SessionID)
|
||||||
rightCard := components.ContentCard(titleStr, rightBody, rightW)
|
rightCard := components.ContentCard(titleStr, rightBody, rightW)
|
||||||
@@ -248,11 +263,10 @@ func (a App) renderSessionDetail(sessions []model.SessionStats, cw, h int) strin
|
|||||||
}
|
}
|
||||||
sel := sessions[cursor]
|
sel := sessions[cursor]
|
||||||
|
|
||||||
headerStyle := lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
mutedStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
|
||||||
|
|
||||||
body := a.renderDetailBody(sel, cw, headerStyle, mutedStyle)
|
body := a.renderDetailBody(sel, cw, mutedStyle)
|
||||||
body = a.applyDetailScroll(body, h-4)
|
body = a.applyDetailScroll(body, h-sessDetailOverhead)
|
||||||
|
|
||||||
title := "Session " + shortID(sel.SessionID)
|
title := "Session " + shortID(sel.SessionID)
|
||||||
return components.ContentCard(title, body, cw)
|
return components.ContentCard(title, body, cw)
|
||||||
@@ -260,21 +274,28 @@ func (a App) renderSessionDetail(sessions []model.SessionStats, cw, h int) strin
|
|||||||
|
|
||||||
// renderDetailBody generates the full detail content for a session.
|
// renderDetailBody generates the full detail content for a session.
|
||||||
// Used by both the split right pane and the full-screen detail view.
|
// Used by both the split right pane and the full-screen detail view.
|
||||||
func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedStyle lipgloss.Style) string {
|
func (a App) renderDetailBody(sel model.SessionStats, w int, mutedStyle lipgloss.Style) string {
|
||||||
t := theme.Active
|
t := theme.Active
|
||||||
innerW := components.CardInnerWidth(w)
|
innerW := components.CardInnerWidth(w)
|
||||||
|
|
||||||
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
// Rich color palette for different data types
|
||||||
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
greenStyle := lipgloss.NewStyle().Foreground(t.Green)
|
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
|
tokenStyle := lipgloss.NewStyle().Foreground(t.Cyan).Background(t.Surface)
|
||||||
|
costStyle := lipgloss.NewStyle().Foreground(t.GreenBright).Background(t.Surface)
|
||||||
|
savingsStyle := lipgloss.NewStyle().Foreground(t.Green).Background(t.Surface).Bold(true)
|
||||||
|
timeStyle := lipgloss.NewStyle().Foreground(t.Magenta).Background(t.Surface)
|
||||||
|
modelStyle := lipgloss.NewStyle().Foreground(t.BlueBright).Background(t.Surface)
|
||||||
|
accentStyle := lipgloss.NewStyle().Foreground(t.AccentBright).Background(t.Surface).Bold(true)
|
||||||
|
dimStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
|
||||||
|
|
||||||
var body strings.Builder
|
var body strings.Builder
|
||||||
body.WriteString(mutedStyle.Render(sel.Project))
|
body.WriteString(accentStyle.Render(sel.Project))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", innerW)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", innerW)))
|
||||||
body.WriteString("\n\n")
|
body.WriteString("\n\n")
|
||||||
|
|
||||||
// Duration line
|
// Duration line with colored values
|
||||||
if !sel.StartTime.IsZero() {
|
if !sel.StartTime.IsZero() {
|
||||||
durStr := cli.FormatDuration(sel.DurationSecs)
|
durStr := cli.FormatDuration(sel.DurationSecs)
|
||||||
timeStr := sel.StartTime.Local().Format("15:04:05")
|
timeStr := sel.StartTime.Local().Format("15:04:05")
|
||||||
@@ -282,28 +303,42 @@ func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedS
|
|||||||
timeStr += " - " + sel.EndTime.Local().Format("15:04:05")
|
timeStr += " - " + sel.EndTime.Local().Format("15:04:05")
|
||||||
}
|
}
|
||||||
timeStr += " " + sel.StartTime.Local().Format("MST")
|
timeStr += " " + sel.StartTime.Local().Format("MST")
|
||||||
fmt.Fprintf(&body, "%s %s (%s)\n",
|
body.WriteString(labelStyle.Render("Duration: "))
|
||||||
labelStyle.Render("Duration:"),
|
body.WriteString(timeStyle.Render(durStr))
|
||||||
valueStyle.Render(durStr),
|
body.WriteString(dimStyle.Render(" ("))
|
||||||
mutedStyle.Render(timeStr))
|
body.WriteString(mutedStyle.Render(timeStr))
|
||||||
|
body.WriteString(dimStyle.Render(")"))
|
||||||
|
body.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
ratio := 0.0
|
ratio := 0.0
|
||||||
if sel.UserMessages > 0 {
|
if sel.UserMessages > 0 {
|
||||||
ratio = float64(sel.APICalls) / float64(sel.UserMessages)
|
ratio = float64(sel.APICalls) / float64(sel.UserMessages)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&body, "%s %s %s %s %s %.1fx\n\n",
|
body.WriteString(labelStyle.Render("Prompts: "))
|
||||||
labelStyle.Render("Prompts:"), valueStyle.Render(cli.FormatNumber(int64(sel.UserMessages))),
|
body.WriteString(valueStyle.Render(cli.FormatNumber(int64(sel.UserMessages))))
|
||||||
labelStyle.Render("API Calls:"), valueStyle.Render(cli.FormatNumber(int64(sel.APICalls))),
|
body.WriteString(dimStyle.Render(" "))
|
||||||
labelStyle.Render("Ratio:"), ratio)
|
body.WriteString(labelStyle.Render("API Calls: "))
|
||||||
|
body.WriteString(tokenStyle.Render(cli.FormatNumber(int64(sel.APICalls))))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(labelStyle.Render("Ratio: "))
|
||||||
|
body.WriteString(accentStyle.Render(fmt.Sprintf("%.1fx", ratio)))
|
||||||
|
body.WriteString("\n\n")
|
||||||
|
|
||||||
// Token breakdown table
|
// Token breakdown table with section header
|
||||||
body.WriteString(headerStyle.Render("TOKEN BREAKDOWN"))
|
sectionStyle := lipgloss.NewStyle().Foreground(t.AccentBright).Background(t.Surface).Bold(true)
|
||||||
|
tableHeaderStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface)
|
||||||
|
|
||||||
|
body.WriteString(sectionStyle.Render("TOKEN BREAKDOWN"))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
typeW, tokW, costW, tableW := tokenTableLayout(innerW)
|
typeW, tokW, costW, tableW := tokenTableLayout(innerW)
|
||||||
body.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %*s %*s", typeW, "Type", tokW, "Tokens", costW, "Cost")))
|
body.WriteString(tableHeaderStyle.Render(fmt.Sprintf("%-*s", typeW, "Type")))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(tableHeaderStyle.Render(fmt.Sprintf("%*s", tokW, "Tokens")))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(tableHeaderStyle.Render(fmt.Sprintf("%*s", costW, "Cost")))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", tableW)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", tableW)))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
|
|
||||||
// Calculate per-type costs (aggregate across models)
|
// Calculate per-type costs (aggregate across models)
|
||||||
@@ -342,37 +377,35 @@ func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedS
|
|||||||
if r.tokens == 0 {
|
if r.tokens == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
body.WriteString(valueStyle.Render(fmt.Sprintf("%-*s %*s %*s",
|
body.WriteString(labelStyle.Render(fmt.Sprintf("%-*s", typeW, truncStr(r.typ, typeW))))
|
||||||
typeW,
|
body.WriteString(dimStyle.Render(" "))
|
||||||
truncStr(r.typ, typeW),
|
body.WriteString(tokenStyle.Render(fmt.Sprintf("%*s", tokW, cli.FormatTokens(r.tokens))))
|
||||||
tokW,
|
body.WriteString(dimStyle.Render(" "))
|
||||||
cli.FormatTokens(r.tokens),
|
body.WriteString(costStyle.Render(fmt.Sprintf("%*s", costW, cli.FormatCost(r.cost))))
|
||||||
costW,
|
|
||||||
cli.FormatCost(r.cost))))
|
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", tableW)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", tableW)))
|
||||||
|
body.WriteString("\n")
|
||||||
|
// Net Cost row - highlighted
|
||||||
|
body.WriteString(accentStyle.Render(fmt.Sprintf("%-*s", typeW, "Net Cost")))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(dimStyle.Render(fmt.Sprintf("%*s", tokW, "")))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(savingsStyle.Render(fmt.Sprintf("%*s", costW, cli.FormatCost(sel.EstimatedCost))))
|
||||||
|
body.WriteString("\n")
|
||||||
|
// Cache Savings row
|
||||||
|
body.WriteString(labelStyle.Render(fmt.Sprintf("%-*s", typeW, "Cache Savings")))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(dimStyle.Render(fmt.Sprintf("%*s", tokW, "")))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(savingsStyle.Render(fmt.Sprintf("%*s", costW, cli.FormatCost(savings))))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
fmt.Fprintf(&body, "%-*s %*s %*s\n",
|
|
||||||
typeW,
|
|
||||||
valueStyle.Render("Net Cost"),
|
|
||||||
tokW,
|
|
||||||
"",
|
|
||||||
costW,
|
|
||||||
greenStyle.Render(cli.FormatCost(sel.EstimatedCost)))
|
|
||||||
fmt.Fprintf(&body, "%-*s %*s %*s\n",
|
|
||||||
typeW,
|
|
||||||
labelStyle.Render("Cache Savings"),
|
|
||||||
tokW,
|
|
||||||
"",
|
|
||||||
costW,
|
|
||||||
greenStyle.Render(cli.FormatCost(savings)))
|
|
||||||
|
|
||||||
// Model breakdown
|
// Model breakdown with colored data
|
||||||
if len(sel.Models) > 0 {
|
if len(sel.Models) > 0 {
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(headerStyle.Render("API CALLS BY MODEL"))
|
body.WriteString(sectionStyle.Render("API CALLS BY MODEL"))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
compactModelTable := innerW < 60
|
compactModelTable := innerW < 60
|
||||||
if compactModelTable {
|
if compactModelTable {
|
||||||
@@ -380,14 +413,14 @@ func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedS
|
|||||||
if modelW < 8 {
|
if modelW < 8 {
|
||||||
modelW = 8
|
modelW = 8
|
||||||
}
|
}
|
||||||
body.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %7s %8s", modelW, "Model", "Calls", "Cost")))
|
body.WriteString(tableHeaderStyle.Render(fmt.Sprintf("%-*s %7s %8s", modelW, "Model", "Calls", "Cost")))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", modelW+7+8+2)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", modelW+7+8+2)))
|
||||||
} else {
|
} else {
|
||||||
modelW := 14
|
modelW := 14
|
||||||
body.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %7s %10s %10s %8s", modelW, "Model", "Calls", "Input", "Output", "Cost")))
|
body.WriteString(tableHeaderStyle.Render(fmt.Sprintf("%-*s %7s %10s %10s %8s", modelW, "Model", "Calls", "Input", "Output", "Cost")))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", modelW+7+10+10+8+4)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", modelW+7+10+10+8+4)))
|
||||||
}
|
}
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
|
|
||||||
@@ -405,20 +438,22 @@ func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedS
|
|||||||
if modelW < 8 {
|
if modelW < 8 {
|
||||||
modelW = 8
|
modelW = 8
|
||||||
}
|
}
|
||||||
body.WriteString(valueStyle.Render(fmt.Sprintf("%-*s %7s %8s",
|
body.WriteString(modelStyle.Render(fmt.Sprintf("%-*s", modelW, truncStr(shortModel(modelName), modelW))))
|
||||||
modelW,
|
body.WriteString(dimStyle.Render(" "))
|
||||||
truncStr(shortModel(modelName), modelW),
|
body.WriteString(valueStyle.Render(fmt.Sprintf("%7s", cli.FormatNumber(int64(mu.APICalls)))))
|
||||||
cli.FormatNumber(int64(mu.APICalls)),
|
body.WriteString(dimStyle.Render(" "))
|
||||||
cli.FormatCost(mu.EstimatedCost))))
|
body.WriteString(costStyle.Render(fmt.Sprintf("%8s", cli.FormatCost(mu.EstimatedCost))))
|
||||||
} else {
|
} else {
|
||||||
modelW := 14
|
modelW := 14
|
||||||
body.WriteString(valueStyle.Render(fmt.Sprintf("%-*s %7s %10s %10s %8s",
|
body.WriteString(modelStyle.Render(fmt.Sprintf("%-*s", modelW, truncStr(shortModel(modelName), modelW))))
|
||||||
modelW,
|
body.WriteString(dimStyle.Render(" "))
|
||||||
truncStr(shortModel(modelName), modelW),
|
body.WriteString(valueStyle.Render(fmt.Sprintf("%7s", cli.FormatNumber(int64(mu.APICalls)))))
|
||||||
cli.FormatNumber(int64(mu.APICalls)),
|
body.WriteString(dimStyle.Render(" "))
|
||||||
cli.FormatTokens(mu.InputTokens),
|
body.WriteString(tokenStyle.Render(fmt.Sprintf("%10s", cli.FormatTokens(mu.InputTokens))))
|
||||||
cli.FormatTokens(mu.OutputTokens),
|
body.WriteString(dimStyle.Render(" "))
|
||||||
cli.FormatCost(mu.EstimatedCost))))
|
body.WriteString(tokenStyle.Render(fmt.Sprintf("%10s", cli.FormatTokens(mu.OutputTokens))))
|
||||||
|
body.WriteString(dimStyle.Render(" "))
|
||||||
|
body.WriteString(costStyle.Render(fmt.Sprintf("%8s", cli.FormatCost(mu.EstimatedCost))))
|
||||||
}
|
}
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
}
|
}
|
||||||
@@ -426,23 +461,23 @@ func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedS
|
|||||||
|
|
||||||
if sel.IsSubagent {
|
if sel.IsSubagent {
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(mutedStyle.Render("(subagent session)"))
|
body.WriteString(dimStyle.Render("(subagent session)"))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subagent drill-down
|
// Subagent drill-down with colors
|
||||||
if subs := a.subagentMap[sel.SessionID]; len(subs) > 0 {
|
if subs := a.subagentMap[sel.SessionID]; len(subs) > 0 {
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(headerStyle.Render(fmt.Sprintf("SUBAGENTS (%d)", len(subs))))
|
body.WriteString(sectionStyle.Render(fmt.Sprintf("SUBAGENTS (%d)", len(subs))))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
|
|
||||||
nameW := innerW - 8 - 10 - 2
|
nameW := innerW - 8 - 10 - 2
|
||||||
if nameW < 10 {
|
if nameW < 10 {
|
||||||
nameW = 10
|
nameW = 10
|
||||||
}
|
}
|
||||||
body.WriteString(headerStyle.Render(fmt.Sprintf("%-*s %8s %10s", nameW, "Agent", "Duration", "Cost")))
|
body.WriteString(tableHeaderStyle.Render(fmt.Sprintf("%-*s %8s %10s", nameW, "Agent", "Duration", "Cost")))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", nameW+8+10+2)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", nameW+8+10+2)))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
|
|
||||||
var totalSubCost float64
|
var totalSubCost float64
|
||||||
@@ -455,31 +490,41 @@ func (a App) renderDetailBody(sel model.SessionStats, w int, headerStyle, mutedS
|
|||||||
}
|
}
|
||||||
agentName = strings.TrimPrefix(agentName, "agent-")
|
agentName = strings.TrimPrefix(agentName, "agent-")
|
||||||
|
|
||||||
body.WriteString(valueStyle.Render(fmt.Sprintf("%-*s %8s %10s",
|
body.WriteString(modelStyle.Render(fmt.Sprintf("%-*s", nameW, truncStr(agentName, nameW))))
|
||||||
nameW,
|
body.WriteString(dimStyle.Render(" "))
|
||||||
truncStr(agentName, nameW),
|
body.WriteString(timeStyle.Render(fmt.Sprintf("%8s", cli.FormatDuration(sub.DurationSecs))))
|
||||||
cli.FormatDuration(sub.DurationSecs),
|
body.WriteString(dimStyle.Render(" "))
|
||||||
cli.FormatCost(sub.EstimatedCost))))
|
body.WriteString(costStyle.Render(fmt.Sprintf("%10s", cli.FormatCost(sub.EstimatedCost))))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
totalSubCost += sub.EstimatedCost
|
totalSubCost += sub.EstimatedCost
|
||||||
totalSubDur += sub.DurationSecs
|
totalSubDur += sub.DurationSecs
|
||||||
}
|
}
|
||||||
|
|
||||||
body.WriteString(mutedStyle.Render(strings.Repeat("─", nameW+8+10+2)))
|
body.WriteString(dimStyle.Render(strings.Repeat("─", nameW+8+10+2)))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
body.WriteString(valueStyle.Render(fmt.Sprintf("%-*s %8s %10s",
|
body.WriteString(accentStyle.Render(fmt.Sprintf("%-*s", nameW, "Combined")))
|
||||||
nameW,
|
body.WriteString(dimStyle.Render(" "))
|
||||||
"Combined",
|
body.WriteString(timeStyle.Render(fmt.Sprintf("%8s", cli.FormatDuration(totalSubDur))))
|
||||||
cli.FormatDuration(totalSubDur),
|
body.WriteString(dimStyle.Render(" "))
|
||||||
cli.FormatCost(totalSubCost))))
|
body.WriteString(savingsStyle.Render(fmt.Sprintf("%10s", cli.FormatCost(totalSubCost))))
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Footer hints with styled keys
|
||||||
body.WriteString("\n")
|
body.WriteString("\n")
|
||||||
|
hintKeyStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.Surface)
|
||||||
|
hintTextStyle := lipgloss.NewStyle().Foreground(t.TextDim).Background(t.Surface)
|
||||||
if w < compactWidth {
|
if w < compactWidth {
|
||||||
body.WriteString(mutedStyle.Render("[/] search [j/k] navigate [J/K] scroll [q] quit"))
|
body.WriteString(hintTextStyle.Render("[") + hintKeyStyle.Render("/") + hintTextStyle.Render("] search [") +
|
||||||
|
hintKeyStyle.Render("j/k") + hintTextStyle.Render("] navigate [") +
|
||||||
|
hintKeyStyle.Render("J/K") + hintTextStyle.Render("] scroll [") +
|
||||||
|
hintKeyStyle.Render("q") + hintTextStyle.Render("] quit"))
|
||||||
} else {
|
} else {
|
||||||
body.WriteString(mutedStyle.Render("[/] search [Enter] expand [j/k] navigate [J/K/^d/^u] scroll [q] quit"))
|
body.WriteString(hintTextStyle.Render("[") + hintKeyStyle.Render("/") + hintTextStyle.Render("] search [") +
|
||||||
|
hintKeyStyle.Render("Enter") + hintTextStyle.Render("] expand [") +
|
||||||
|
hintKeyStyle.Render("j/k") + hintTextStyle.Render("] navigate [") +
|
||||||
|
hintKeyStyle.Render("J/K/^d/^u") + hintTextStyle.Render("] scroll [") +
|
||||||
|
hintKeyStyle.Render("q") + hintTextStyle.Render("] quit"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return body.String()
|
return body.String()
|
||||||
@@ -495,8 +540,8 @@ func shortID(id string) string {
|
|||||||
// applyDetailScroll applies the detail pane scroll offset to a rendered body string.
|
// applyDetailScroll applies the detail pane scroll offset to a rendered body string.
|
||||||
// visibleH is the number of lines that fit in the card body area.
|
// visibleH is the number of lines that fit in the card body area.
|
||||||
func (a App) applyDetailScroll(body string, visibleH int) string {
|
func (a App) applyDetailScroll(body string, visibleH int) string {
|
||||||
if visibleH < 5 {
|
if visibleH < sessMinVisible {
|
||||||
visibleH = 5
|
visibleH = sessMinVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(body, "\n")
|
lines := strings.Split(body, "\n")
|
||||||
@@ -526,7 +571,7 @@ func (a App) applyDetailScroll(body string, visibleH int) string {
|
|||||||
// Count includes the line we're replacing + lines past the viewport.
|
// Count includes the line we're replacing + lines past the viewport.
|
||||||
if endIdx < len(lines) {
|
if endIdx < len(lines) {
|
||||||
unseen := len(lines) - endIdx + 1
|
unseen := len(lines) - endIdx + 1
|
||||||
dimStyle := lipgloss.NewStyle().Foreground(theme.Active.TextDim)
|
dimStyle := lipgloss.NewStyle().Foreground(theme.Active.TextDim).Background(theme.Active.Surface)
|
||||||
visible[len(visible)-1] = dimStyle.Render(fmt.Sprintf("... %d more", unseen))
|
visible[len(visible)-1] = dimStyle.Render(fmt.Sprintf("... %d more", unseen))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func newSettingsInput() textinput.Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a App) settingsStartEdit() (tea.Model, tea.Cmd) {
|
func (a App) settingsStartEdit() (tea.Model, tea.Cmd) {
|
||||||
cfg, _ := config.Load()
|
cfg := loadConfigOrDefault()
|
||||||
a.settings.editing = true
|
a.settings.editing = true
|
||||||
a.settings.saved = false
|
a.settings.saved = false
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ func (a App) updateSettingsInput(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) settingsSave() {
|
func (a *App) settingsSave() {
|
||||||
cfg, _ := config.Load()
|
cfg := loadConfigOrDefault()
|
||||||
val := strings.TrimSpace(a.settings.input.Value())
|
val := strings.TrimSpace(a.settings.input.Value())
|
||||||
|
|
||||||
switch a.settings.cursor {
|
switch a.settings.cursor {
|
||||||
@@ -176,13 +176,15 @@ func (a *App) settingsSave() {
|
|||||||
|
|
||||||
func (a App) renderSettingsTab(cw int) string {
|
func (a App) renderSettingsTab(cw int) string {
|
||||||
t := theme.Active
|
t := theme.Active
|
||||||
cfg, _ := config.Load()
|
cfg := loadConfigOrDefault()
|
||||||
|
|
||||||
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted)
|
labelStyle := lipgloss.NewStyle().Foreground(t.TextMuted).Background(t.Surface)
|
||||||
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary)
|
valueStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface)
|
||||||
selectedStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.Surface).Bold(true)
|
selectedStyle := lipgloss.NewStyle().Foreground(t.TextPrimary).Background(t.SurfaceBright).Bold(true)
|
||||||
accentStyle := lipgloss.NewStyle().Foreground(t.Accent)
|
selectedLabelStyle := lipgloss.NewStyle().Foreground(t.Accent).Background(t.SurfaceBright).Bold(true)
|
||||||
greenStyle := lipgloss.NewStyle().Foreground(t.Green)
|
accentStyle := lipgloss.NewStyle().Foreground(t.AccentBright).Background(t.Surface)
|
||||||
|
greenStyle := lipgloss.NewStyle().Foreground(t.GreenBright).Background(t.Surface)
|
||||||
|
markerStyle := lipgloss.NewStyle().Foreground(t.AccentBright).Background(t.SurfaceBright)
|
||||||
|
|
||||||
type field struct {
|
type field struct {
|
||||||
label string
|
label string
|
||||||
@@ -235,23 +237,39 @@ func (a App) renderSettingsTab(cw int) string {
|
|||||||
for i, f := range fields {
|
for i, f := range fields {
|
||||||
// Show text input if currently editing this field
|
// Show text input if currently editing this field
|
||||||
if a.settings.editing && i == a.settings.cursor {
|
if a.settings.editing && i == a.settings.cursor {
|
||||||
formBody.WriteString(accentStyle.Render(fmt.Sprintf("> %-18s ", f.label)))
|
formBody.WriteString(markerStyle.Render("▸ "))
|
||||||
|
formBody.WriteString(accentStyle.Render(fmt.Sprintf("%-18s ", f.label)))
|
||||||
formBody.WriteString(a.settings.input.View())
|
formBody.WriteString(a.settings.input.View())
|
||||||
formBody.WriteString("\n")
|
formBody.WriteString("\n")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
line := fmt.Sprintf("%-20s %s", f.label+":", f.value)
|
|
||||||
if i == a.settings.cursor {
|
if i == a.settings.cursor {
|
||||||
formBody.WriteString(selectedStyle.Render(line))
|
// Selected row with marker and highlight
|
||||||
|
marker := markerStyle.Render("▸ ")
|
||||||
|
label := selectedLabelStyle.Render(fmt.Sprintf("%-18s ", f.label+":"))
|
||||||
|
value := selectedStyle.Render(f.value)
|
||||||
|
formBody.WriteString(marker)
|
||||||
|
formBody.WriteString(label)
|
||||||
|
formBody.WriteString(value)
|
||||||
|
// Use lipgloss.Width() for correct visual width calculation
|
||||||
|
usedWidth := lipgloss.Width(marker) + lipgloss.Width(label) + lipgloss.Width(value)
|
||||||
|
innerW := components.CardInnerWidth(cw)
|
||||||
|
padLen := innerW - usedWidth
|
||||||
|
if padLen > 0 {
|
||||||
|
formBody.WriteString(lipgloss.NewStyle().Background(t.SurfaceBright).Render(strings.Repeat(" ", padLen)))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
formBody.WriteString(labelStyle.Render(fmt.Sprintf("%-20s ", f.label+":")) + valueStyle.Render(f.value))
|
// Normal row
|
||||||
|
formBody.WriteString(lipgloss.NewStyle().Background(t.Surface).Render(" "))
|
||||||
|
formBody.WriteString(labelStyle.Render(fmt.Sprintf("%-18s ", f.label+":")))
|
||||||
|
formBody.WriteString(valueStyle.Render(f.value))
|
||||||
}
|
}
|
||||||
formBody.WriteString("\n")
|
formBody.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.settings.saveErr != nil {
|
if a.settings.saveErr != nil {
|
||||||
warnStyle := lipgloss.NewStyle().Foreground(t.Orange)
|
warnStyle := lipgloss.NewStyle().Foreground(t.Orange).Background(t.Surface)
|
||||||
formBody.WriteString("\n")
|
formBody.WriteString("\n")
|
||||||
formBody.WriteString(warnStyle.Render(fmt.Sprintf("Save failed: %s", a.settings.saveErr)))
|
formBody.WriteString(warnStyle.Render(fmt.Sprintf("Save failed: %s", a.settings.saveErr)))
|
||||||
} else if a.settings.saved {
|
} else if a.settings.saved {
|
||||||
|
|||||||
Reference in New Issue
Block a user