kunchenguid--no-mistakes
26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
153 行
4.7 KiB
Go
153 行
4.7 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/kunchenguid/no-mistakes/internal/cimonitor"
|
|
"github.com/kunchenguid/no-mistakes/internal/ipc"
|
|
"github.com/kunchenguid/no-mistakes/internal/types"
|
|
)
|
|
|
|
// isCIActive returns true if the CI step is currently running.
|
|
func isCIActive(steps []ipc.StepResultInfo) bool {
|
|
for _, s := range steps {
|
|
if s.StepName == types.StepCI {
|
|
switch s.Status {
|
|
case types.StepStatusRunning:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ciStepStatus returns the current status of the CI step.
|
|
func ciStepStatus(steps []ipc.StepResultInfo) types.StepStatus {
|
|
for _, s := range steps {
|
|
if s.StepName == types.StepCI {
|
|
return s.Status
|
|
}
|
|
}
|
|
return types.StepStatusPending
|
|
}
|
|
|
|
// extractPRFromLogs extracts the PR number from CI log messages.
|
|
// Looks for the "monitoring CI for PR #42" pattern. Returns empty if not found.
|
|
func extractPRFromLogs(logs []string) string {
|
|
for _, line := range logs {
|
|
if idx := strings.Index(line, "PR #"); idx >= 0 {
|
|
rest := line[idx+4:]
|
|
end := strings.IndexAny(rest, " ()\n")
|
|
if end < 0 {
|
|
end = len(rest)
|
|
}
|
|
num := rest[:end]
|
|
if num != "" {
|
|
return num
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ciActivity summarizes what the CI step has been doing based on logs. It is an
|
|
// alias of cimonitor.Activity so the TUI and the agent-facing axi commands read
|
|
// CI state through the exact same parser.
|
|
type ciActivity = cimonitor.Activity
|
|
|
|
// parseCIActivity extracts structured activity from CI log messages. It defers
|
|
// to cimonitor so the TUI never drifts from how axi interprets the same logs.
|
|
func parseCIActivity(logs []string) ciActivity {
|
|
return cimonitor.ParseActivity(logs)
|
|
}
|
|
|
|
// renderCIView renders the CI-specific monitoring view.
|
|
// Shown instead of generic findings when the CI step is active.
|
|
func renderCIView(run *ipc.RunInfo, steps []ipc.StepResultInfo, findings string, logs []string, width int) string {
|
|
return renderCIViewWithSelection(run, steps, findings, logs, width, -1, 0, nil)
|
|
}
|
|
|
|
func renderCIViewWithSelection(run *ipc.RunInfo, steps []ipc.StepResultInfo, findings string, logs []string, width int, height int, cursor int, selected map[string]bool) string {
|
|
var b strings.Builder
|
|
|
|
boxWidth := width
|
|
if boxWidth < 20 {
|
|
boxWidth = 80
|
|
}
|
|
contentWidth := boxWidth - 4 // account for box border + padding
|
|
|
|
dimStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(ansiBrightBlack))
|
|
if run != nil && run.PRURL != nil && *run.PRURL != "" {
|
|
prText, _ := cutText(shortPRLabel(*run.PRURL), contentWidth)
|
|
b.WriteString(dimStyle.Render(prText) + "\n")
|
|
} else if num := extractPRFromLogs(logs); num != "" {
|
|
prText, _ := cutText("PR #"+num, contentWidth)
|
|
b.WriteString(dimStyle.Render(prText) + "\n")
|
|
}
|
|
|
|
// State indicator.
|
|
status := ciStepStatus(steps)
|
|
activity := parseCIActivity(logs)
|
|
|
|
b.WriteString("\n")
|
|
|
|
switch status {
|
|
case types.StepStatusRunning:
|
|
if activity.AutoFixing {
|
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color(ansiBlue))
|
|
b.WriteString(style.Render("\u2699 Auto-fixing CI failures...") + "\n")
|
|
} else if activity.Ready {
|
|
style := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(ansiGreen))
|
|
b.WriteString(style.Render("✓ Checks passed") + "\n")
|
|
dim := lipgloss.NewStyle().Foreground(lipgloss.Color(ansiBrightBlack))
|
|
b.WriteString(dim.Render("still monitoring until merged or closed") + "\n")
|
|
} else {
|
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color(ansiGreen))
|
|
b.WriteString(style.Render("◉ Monitoring CI checks...") + "\n")
|
|
}
|
|
}
|
|
|
|
// CI auto-fix count.
|
|
if activity.CIFixes > 0 {
|
|
b.WriteString(dimStyle.Render(fmt.Sprintf("CI auto-fixes: %d", activity.CIFixes)) + "\n")
|
|
}
|
|
|
|
// Last activity, truncated to fit inside the box.
|
|
if activity.LastEvent != "" {
|
|
eventText := "Latest: " + activity.LastEvent
|
|
eventText, _ = cutText(eventText, contentWidth)
|
|
b.WriteString(dimStyle.Render(eventText) + "\n")
|
|
}
|
|
|
|
// Log tail during monitoring.
|
|
// Dynamically fill available height: subtract box borders and fixed content lines.
|
|
if len(logs) > 0 && height >= 0 {
|
|
// Count fixed lines already written above.
|
|
fixedLines := 2 // box top + bottom borders
|
|
fixedLines += lipgloss.Height(b.String())
|
|
fixedLines++ // blank line before log tail
|
|
|
|
logLines := height - fixedLines
|
|
if logLines < 1 {
|
|
logLines = 0
|
|
}
|
|
|
|
if logLines > 0 {
|
|
b.WriteString("\n")
|
|
for _, line := range renderLogTail(logs, contentWidth, logLines) {
|
|
b.WriteString(line + "\n")
|
|
}
|
|
}
|
|
} else if len(logs) > 0 {
|
|
// No height info - show a reasonable default.
|
|
b.WriteString("\n")
|
|
for _, line := range renderLogTail(logs, contentWidth, 10) {
|
|
b.WriteString(line + "\n")
|
|
}
|
|
}
|
|
|
|
return renderBox("CI", b.String(), boxWidth)
|
|
}
|