Skip to content
Merged
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
16 changes: 13 additions & 3 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"log"
"runtime"

tea "github.com/charmbracelet/bubbletea/v2"
"github.com/spf13/cobra"
Expand All @@ -13,14 +14,23 @@ var (

func main() {
var rootCmd = &cobra.Command{
Use: "gradient-engineer [flags] PLAYBOOK_NAME",
Use: "gradient-engineer [flags] [PLAYBOOK_NAME]",
Short: "Run diagnostic playbooks using gradient engineer toolbox",
Long: `Gradient Engineer runs diagnostic playbooks by downloading and executing
toolbox commands. The toolbox is automatically downloaded from the specified
repository based on your platform (OS and architecture).`,
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
playbookName := args[0]
var playbookName string
if len(args) > 0 {
playbookName = args[0]
} else {
if runtime.GOOS == "linux" {
playbookName = "60-second-linux"
} else {
playbookName = "60-second-darwin"
}
}

// Create a new toolbox instance
tb := NewToolbox(toolboxRepo, playbookName)
Expand Down
96 changes: 60 additions & 36 deletions app/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import (
)

var (
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212"))
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("0")) // foreground black; background will be per-char gradient
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("255"))
pendingStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
runningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("69"))
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
footerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Italic(true)
footerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("250")).Italic(true)
descStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
)

// commandStatus represents the execution status of a diagnostic command.
Expand Down Expand Up @@ -80,6 +80,12 @@ type model struct {
done bool

summarizer *Summarizer

// Time it took to execute all commands (seconds), captured when summarization starts
execSeconds float64

// Request a one-time scroll to bottom after next SetContent in View
requestScrollToBottom bool
}

// NewModel constructs a model initialised with all diagnostic commands in a
Expand Down Expand Up @@ -194,9 +200,11 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if allDone {
if !m.summarizing && m.summary == "" {
m.execSeconds = time.Since(m.startTime).Seconds()
m.requestScrollToBottom = true
// If summarizer is disabled (no API key), skip summarization and show a notice.
if m.summarizer == nil || m.summarizer.disabled {
m.summaryNotice = "No API key provided; skipping AI summary."
m.summaryNotice = "No API key provided; skipping AI summary.\nSet the API key with OPENAI_API_KEY, OPENROUTER_API_KEY, or ANTHROPIC_API_KEY."
return m, nil
}
m.summarizing = true
Expand Down Expand Up @@ -276,6 +284,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *model) View() string {
// Build the content string and assign it to the viewport.
m.vp.SetContent(m.generateContent())
if m.requestScrollToBottom {
m.vp.GotoBottom()
m.requestScrollToBottom = false
}
return m.vp.View()
}

Expand Down Expand Up @@ -318,7 +330,16 @@ func (m *model) generateContent() string {
default:
lineStyle = pendingStyle
}
cmdBuf.WriteString(lineStyle.Render(fmt.Sprintf("%s %s", icon, cmd.Display)))
// Render command and lighter description
cmdText := cmd.Command
if cmd.Spec != nil && strings.TrimSpace(cmd.Spec.Command) != "" {
cmdText = cmd.Spec.Command
}
line := lineStyle.Render(fmt.Sprintf("%s %s", icon, cmdText))
if strings.TrimSpace(cmd.Display) != "" {
line += " " + descStyle.Render("— "+cmd.Display)
}
cmdBuf.WriteString(line)
cmdBuf.WriteString("\n")

if m.showDetails {
Expand All @@ -338,23 +359,36 @@ func (m *model) generateContent() string {
}

// Strip final \n from cmdBuf if present
cmdStr := cmdBuf.String()
if strings.HasSuffix(cmdStr, "\n") {
cmdStr = cmdStr[:len(cmdStr)-1]
}
cmdStr := strings.TrimSuffix(cmdBuf.String(), "\n")
cmdBuf.Reset()
cmdBuf.WriteString(cmdStr)

// Border the commands with a title
sectionTitle := renderGradientHeader(" 60-second Linux analysis ", time.Since(m.startTime).Seconds())
commandsBox := sectionTitle + "\n\n" + cmdBuf.String()
// Border the commands with a title (plain header, with spinner while running or a tick when finished)
headerTitle := titleStyle.Render("60-second Linux analysis")
var header string
if m.execSeconds == 0 {
header = fmt.Sprintf("%s %s", m.spin.View(), headerTitle)
} else {
header = fmt.Sprintf("%s %s %s", successStyle.Render(iconSuccess), headerTitle, descStyle.Render(fmt.Sprintf("(finished in %.1f seconds)", m.execSeconds)))
}
commandsBox := header + "\n\n" + cmdBuf.String()

// Assemble full UI
var b strings.Builder
b.WriteString(generateBanner(time.Since(m.startTime).Seconds()))

b.WriteString("\n")
b.WriteString(footerStyle.Render("Tab: toggle details; q: quit; up/down or mouse: scroll"))

b.WriteString("\n\n")
b.WriteString(commandsBox)

// If we have finished executing commands, show the elapsed time above the summary section
if m.execSeconds > 0 {
b.WriteString("\n\n")
b.WriteString(successStyle.Render(fmt.Sprintf("Executing commands finished in %.1f seconds.", m.execSeconds)))
}

if m.summarizing {
b.WriteString("\n\n")
b.WriteString(runningStyle.Render(fmt.Sprintf("%s Summarizing results with AI…", m.spin.View())))
Expand All @@ -374,10 +408,6 @@ func (m *model) generateContent() string {
b.WriteString(errorStyle.Render(fmt.Sprintf("LLM error: %v", m.summaryErr)))
}

b.WriteString("\n\n")
b.WriteString(footerStyle.Render("Tab: toggle details q: quit"))
b.WriteString("\n")

return b.String()
}

Expand All @@ -392,24 +422,14 @@ func indent(text, prefix string) string {

// ASCII banner lines for Gradient Engineer logo.
var bannerLines = []string{
" ░██ ░██ ░██ ",
" ░██ ░██ ",
" ░████████ ░██░████ ░██████ ░████████ ░██ ░███████ ░████████ ░████████ ",
"░██ ░██ ░███ ░██ ░██ ░██ ░██░██ ░██ ░██ ░██ ░██ ",
"░██ ░██ ░██ ░███████ ░██ ░██ ░██░█████████ ░██ ░██ ░██ ",
"░██ ░███ ░██ ░██ ░██ ░██ ░███ ░██░██ ░██ ░██ ░██ ",
" ░█████░██ ░██ ░█████░██ ░█████░██ ░██ ░███████ ░██ ░██ ░████ ",
" ░██ ",
" ░███████ ",
" ░██ ",
" ",
" ░███████ ░████████ ░████████ ░██░████████ ░███████ ░███████ ░██░████ ",
"░██ ░██ ░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ░██ ░██ ░██ ░███ ",
"░█████████ ░██ ░██ ░██ ░██ ░██░██ ░██ ░█████████ ░█████████ ░██ ",
"░██ ░██ ░██ ░██ ░███ ░██░██ ░██ ░██ ░██ ░██ ",
" ░███████ ░██ ░██ ░█████░██ ░██░██ ░██ ░███████ ░███████ ░██ ",
" ░██ ",
" ░███████ ",
" ▗▄▄▖▗▄▄▖ ▗▄▖ ▗▄▄▄ ▗▄▄▄▖▗▄▄▄▖▗▖ ▗▖▗▄▄▄▖ ",
" ▐▌ ▐▌ ▐▌▐▌ ▐▌▐▌ █ █ ▐▌ ▐▛▚▖▐▌ █ ",
" ▐▌▝▜▌▐▛▀▚▖▐▛▀▜▌▐▌ █ █ ▐▛▀▀▘▐▌ ▝▜▌ █ ",
" ▝▚▄▞▘▐▌ ▐▌▐▌ ▐▌▐▙▄▄▀▗▄█▄▖▐▙▄▄▖▐▌ ▐▌ █ ",
" ▗▄▄▄▖▗▖ ▗▖ ▗▄▄▖▗▄▄▄▖▗▖ ▗▖▗▄▄▄▖▗▄▄▄▖▗▄▄▖ ",
" ▐▌ ▐▛▚▖▐▌▐▌ █ ▐▛▚▖▐▌▐▌ ▐▌ ▐▌ ▐▌",
" ▐▛▀▀▘▐▌ ▝▜▌▐▌▝▜▌ █ ▐▌ ▝▜▌▐▛▀▀▘▐▛▀▀▘▐▛▀▚▖",
" ▐▙▄▄▖▐▌ ▐▌▝▚▄▞▘▗▄█▄▖▐▌ ▐▌▐▙▄▄▖▐▙▄▄▖▐▌ ▐▌",
}

// Convert HSV to RGB (0-360, 0-1, 0-1) output uint8 components.
Expand Down Expand Up @@ -441,7 +461,9 @@ func hsvToRGB(h, s, v float64) (uint8, uint8, uint8) {
func renderGradientHeader(text string, t float64) string {
var b strings.Builder
for i, ch := range text {
progress := math.Mod(float64(i)/100+t*0.1, 1.0)
progress := float64(i)/100 + t*-0.07
progress += math.Ceil(math.Abs(progress))
progress = math.Mod(progress, 1.0)
hue := progress * 360.0
// lower brightness for less intense colors
r, g, c := hsvToRGB(hue, 0.8, 0.5)
Expand All @@ -459,7 +481,9 @@ func generateBanner(t float64) string {
var b strings.Builder
for _, line := range bannerLines {
for i, ch := range line {
progress := math.Mod(float64(i)/float64(len(line))+t*0.10+0.5, 1.0)
progress := float64(i)/float64(len(line)) + t*-0.07 + 0.5
progress += math.Ceil(math.Abs(progress))
progress = math.Mod(progress, 1.0)
hue := progress * 360.0
r, g, cc := hsvToRGB(hue, 1.0, 1.0)
colorStr := fmt.Sprintf("#%02X%02X%02X", r, g, cc)
Expand Down
20 changes: 10 additions & 10 deletions playbook/60-second-darwin.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ system_prompt: |
When recommending actions, prioritize practical steps.
commands:
- command: uptime
description: System uptime and load averages
description: System uptime, load averages
- command: log show --style syslog --last 1m
description: Recent system log entries (last 1 minute)
description: Recent system log entries
- command: vm_stat 1
description: Virtual memory paging, 1s updates
description: Virtual memory paging
- command: ps -M -o pid,%cpu,comm -r
description: Threads sorted by CPU to spot hot single-thread bottlenecks
description: Threads sorted by CPU
- command: top -l 999 -s 1 -o cpu
description: Per-process CPU usage, 1s rolling view
description: Per-process CPU usage
- command: iostat -d -w 1
description: Disk I/O per device, 1s updates (KB/t, tps, MB/s)
description: Disk I/O per device
- command: memory_pressure -Q
description: Memory pressure summary (free, purgeable, compressed)
description: Memory pressure summary
- command: netstat -w 1 -i
description: Network device statistics for all interfaces, 1s updates
description: Network device statistics
- command: netstat -s -p tcp
description: TCP health snapshot (retransmits, connection stats)
description: TCP health snapshot
- command: top -l 1
description: One-shot system snapshot (CPU breakdown, processes, PhysMem)
description: Top processes snapshot


16 changes: 8 additions & 8 deletions playbook/60-second-linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,21 @@ system_prompt: |
When recommending actions, prioritize practical steps.
commands:
- command: uptime
description: System uptime and load averages
description: System uptime, load averages
- command: vmstat 1
description: Virtual memory statistics, 1s updates
description: Virtual memory statistics
- command: mpstat -P ALL 1
description: CPU utilization per core, 1s
description: CPU utilization per core
- command: pidstat 1
description: Per-process CPU usage, 1s
description: Per-process CPU usage
- command: iostat -xz 1
description: Extended I/O statistics, 1s
description: Extended I/O statistics
- command: free -m
description: Memory usage in MiB
description: Memory usage
- command: sar -n DEV 1
description: Network device statistics, 1s
description: Network device statistics
- command: sar -n TCP,ETCP 1
description: TCP counters and errors, 1s
description: TCP counters and errors
- command: top -b -n 1
description: Top processes snapshot
- command: dmesg
Expand Down