Project>project overview from the dashboard endpoint; tools chooser drawn in Board>board; docs for Shell and t

This commit is contained in:
Sam
2026-11-10 21:48:03 +11:00
parent 3d31243317
commit 3e90ca82ab
14 changed files with 1225 additions and 9 deletions

View File

@@ -19,6 +19,7 @@ import (
)
type projectMsg struct {
dash paperclip.Dashboard
project paperclip.Project
cfg map[string]string
agents []paperclip.Agent
@@ -38,6 +39,7 @@ type projectModel struct {
client *paperclip.Client
folder string
dash paperclip.Dashboard
project paperclip.Project
cfg map[string]string
agents []paperclip.Agent
@@ -53,7 +55,7 @@ type projectModel struct {
width, height int
}
var projectTabs = []string{"facts", "costs", "settings"}
var projectTabs = []string{"overview", "facts", "costs", "settings"}
func newProjectModel(c *paperclip.Client, folder string) projectModel {
return projectModel{client: c, folder: folder}
@@ -96,7 +98,8 @@ func (m projectModel) load() tea.Cmd {
}
costs = append(costs, taskCost{t.Identifier, t.Status, cost.CostCents, cost.RunCount})
}
return projectMsg{project: proj, cfg: cfg, agents: agents, costs: costs}
dash, _ := c.DashboardSummary()
return projectMsg{project: proj, cfg: cfg, agents: agents, costs: costs, dash: dash}
}
}
@@ -182,6 +185,7 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case projectMsg:
m.dash = msg.dash
m.project = msg.project
m.cfg = msg.cfg
m.agents = msg.agents
@@ -225,8 +229,10 @@ func (m projectModel) View() string {
var body []string
switch m.tab {
case 0:
body = m.factLines()
body = m.overviewLines(m.width - 4)
case 1:
body = m.factLines()
case 2:
body = m.costLines()
default:
body = m.settingLines()
@@ -268,6 +274,82 @@ func (m projectModel) row(label, value string) string {
lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(value)
}
// overviewLines is the company dashboard, drawn as numbers a person can act on
// and a sparkline of the last two weeks of runs. Everything here comes from
// GET /api/companies/{id}/dashboard in one call.
func (m projectModel) overviewLines(width int) []string {
d := m.dash
out := []string{"", groupHeading("agents", -1, width), ""}
out = append(out, " "+stat("active", d.Agents.Active, active.Text)+
" "+stat("running", d.Agents.Running, active.Info)+
" "+stat("paused", d.Agents.Paused, active.Warning)+
" "+stat("error", d.Agents.Error, active.Error))
out = append(out, "", groupHeading("tasks", -1, width), "")
out = append(out, " "+stat("open", d.Tasks.Open, active.Text)+
" "+stat("in progress", d.Tasks.InProgress, active.Info)+
" "+stat("blocked", d.Tasks.Blocked, active.Error)+
" "+stat("done", d.Tasks.Done, active.Success))
out = append(out, "", groupHeading("money, this month", -1, width), "")
budget := "no company budget set"
if d.Costs.MonthBudgetCents > 0 {
budget = fmt.Sprintf("%.0f%% of %d cents", d.Costs.MonthUtilizationPercent, d.Costs.MonthBudgetCents)
}
out = append(out, " "+stat("cents spent", d.Costs.MonthSpendCents, active.Text)+" "+
mutedStyle2().Render(budget))
out = append(out, " "+stat("pending approvals", d.PendingApprovals, active.Warning))
if d.Budgets.ActiveIncidents > 0 || d.Budgets.PausedAgents > 0 || d.Budgets.PausedProjects > 0 {
out = append(out, " "+stat("budget incidents", d.Budgets.ActiveIncidents, active.Error)+
mutedStyle2().Render(fmt.Sprintf(" paused: %d agent(s), %d project(s)",
d.Budgets.PausedAgents, d.Budgets.PausedProjects)))
}
out = append(out, "", groupHeading("runs, last two weeks", -1, width), "")
out = append(out, " "+sparkline(d.RunActivity))
if n := len(d.RunActivity); n > 0 {
first, last := d.RunActivity[0].Date, d.RunActivity[n-1].Date
out = append(out, dimStyle().Render(" "+first+" → "+last+" one bar per day, taller is more runs"))
}
out = append(out, "")
out = append(out, dimStyle().Render(" read from Paperclip's own dashboard, not counted here."))
return out
}
// stat is a label and a number, the number first because it is what you look at.
func stat(label string, value int, colour string) string {
return lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Bold(true).Render(fmt.Sprintf("%d", value)) +
" " + mutedStyle2().Render(label)
}
// sparkline draws one bar per day using block characters, scaled so the busiest
// day fills the height. No chart library, no second language: the data is in
// memory and the terminal already knows how to draw blocks.
func sparkline(points []struct {
Date string `json:"date"`
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
}) string {
bars := []rune("▁▂▃▄▅▆▇█")
peak := 0
for _, p := range points {
if p.Total > peak {
peak = p.Total
}
}
if peak == 0 {
return mutedStyle2().Render("no runs in the last two weeks")
}
var b strings.Builder
for _, p := range points {
idx := int(float64(p.Total) / float64(peak) * float64(len(bars)-1))
colour := active.Success
if p.Failed > 0 {
colour = active.Warning
}
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(string(bars[idx])))
}
return b.String()
}
func (m projectModel) factLines() []string {
out := []string{""}
if m.project.Name != "" {