Commit b4c6cdb

Eric Bower  ·  2026-08-17 10:20:53 -0400 EDT
parent 2b62b2d
chore(ui): each task get 5 lines of history
3 files changed,  +120, -13
M run.sh
+53, -7
......@@ -32,6 +32,7 @@ type TaskUIState struct {
3232 Duration time.Duration
3333 ExitCode string
3434 LastOutput string // Last non-empty line for running peek
35+ RunningOutput []string // Captured recent output lines while running (up to 5)
3536 ErrorPreview []string // Captured error lines on failure
3637 PredictedDur time.Duration
3738 PrevStatus string
......@@ -134,15 +135,30 @@ func truncateLine(s string, maxWidth int) string {
134135 return string(runes[:maxWidth])
135136 }
136137
137-func extractLastOutputLine(raw string) string {
138+func extractLastOutputLines(raw string, maxLines int) []string {
138139 clean := stripANSI(raw)
139140 lines := strings.Split(clean, "\n")
140- for i := len(lines) - 1; i >= 0; i-- {
141- line := strings.TrimSpace(lines[i])
141+ var nonEmpties []string
142+ for _, l := range lines {
143+ line := strings.TrimSpace(l)
142144 if line != "" && !strings.HasPrefix(line, "step starting") && !strings.HasPrefix(line, "step completed") {
143- return line
145+ nonEmpties = append(nonEmpties, line)
144146 }
145147 }
148+ if len(nonEmpties) == 0 {
149+ return nil
150+ }
151+ if maxLines > 0 && len(nonEmpties) > maxLines {
152+ nonEmpties = nonEmpties[len(nonEmpties)-maxLines:]
153+ }
154+ return nonEmpties
155+}
156+
157+func extractLastOutputLine(raw string) string {
158+ lines := extractLastOutputLines(raw, 1)
159+ if len(lines) > 0 {
160+ return lines[0]
161+ }
146162 return ""
147163 }
148164
......@@ -339,7 +355,15 @@ func (ui *PipelineUIState) Render(frame int) string {
339355 }
340356
341357 fmt.Fprintf(&sb, " %c %-8s %s %s%s\n", spinnerRune, task.Name, formatShortDuration(taskDur), predStr, barPart)
342- if task.LastOutput != "" {
358+ if len(task.RunningOutput) > 0 {
359+ for i, line := range task.RunningOutput {
360+ if i == len(task.RunningOutput)-1 {
361+ fmt.Fprintf(&sb, " └─ %s\n", line)
362+ } else {
363+ fmt.Fprintf(&sb, " │ %s\n", line)
364+ }
365+ }
366+ } else if task.LastOutput != "" {
343367 fmt.Fprintf(&sb, " └─ %s\n", task.LastOutput)
344368 }
345369
......@@ -440,7 +464,11 @@ func (ui *PipelineUIState) RenderSummary(reportPath string, exitCode int) string
440464 if task.ExitCode == "" {
441465 exitStr = "exit 1"
442466 }
443- fmt.Fprintf(&sb, " ✖ %-9s %6s %s (see error preview above)\n", task.Name, formatShortDuration(task.Duration), exitStr)
467+ fmt.Fprintf(&sb, " ✖ %-9s %6s %s\n", task.Name, formatShortDuration(task.Duration), exitStr)
468+ if len(task.ErrorPreview) > 0 {
469+ sb.WriteString(renderErrorPreviewBox(task.ErrorPreview, 65))
470+ sb.WriteString("\n")
471+ }
444472 default:
445473 fmt.Fprintf(&sb, " ✖ %-9s cancelled\n", task.Name)
446474 }
......@@ -754,7 +782,10 @@ func waitAndReportUI(cfg *Cfg, log *slog.Logger, repoName, jobID, eventType, bra
754782 ctrl.ui.mu.Lock()
755783 if task, ok := ctrl.ui.Tasks[s.Short]; ok {
756784 if s.Ended == "" {
757- task.LastOutput = extractLastOutputLine(hist)
785+ task.RunningOutput = extractLastOutputLines(hist, 5)
786+ if len(task.RunningOutput) > 0 {
787+ task.LastOutput = task.RunningOutput[len(task.RunningOutput)-1]
788+ }
758789 } else if s.ExitCode != "0" && len(task.ErrorPreview) == 0 {
759790 task.ErrorPreview = extractErrorPreview(hist, 5)
760791 }
......@@ -797,6 +828,21 @@ func waitAndReportUI(cfg *Cfg, log *slog.Logger, repoName, jobID, eventType, bra
797828 }
798829 }
799830
831+ for _, s := range finalSessions {
832+ if s.Short == "runner" || s.Short == "" {
833+ continue
834+ }
835+ if s.ExitCode != "0" && s.ExitCode != "" {
836+ ctrl.ui.mu.Lock()
837+ if task, ok := ctrl.ui.Tasks[s.Short]; ok && len(task.ErrorPreview) == 0 {
838+ if hist, err := fetchHistoryPlain(s.Name); err == nil && hist != "" {
839+ task.ErrorPreview = extractErrorPreview(hist, 5)
840+ }
841+ }
842+ ctrl.ui.mu.Unlock()
843+ }
844+ }
845+
800846 exitCode, overallStatus := resolveJobExitCode(finalSessions)
801847
802848 // Update historical stats.json
+66, -5
......@@ -91,10 +91,18 @@ func TestUI_EarlyFailurePeekBox(t *testing.T) {
9191 }
9292
9393 func TestUI_LiveActivityPeekExtraction(t *testing.T) {
94- rawHistory := "step starting\n\n \x1b[32mrunning golangci-lint on 28 packages...\x1b[0m\n\n"
94+ rawHistory := "step starting\n\n \x1b[32mline 1\x1b[0m\nline 2\nline 3\nline 4\nline 5\nline 6\n\n"
9595 peek := extractLastOutputLine(rawHistory)
96- if peek != "running golangci-lint on 28 packages..." {
97- t.Errorf("expected clean peek line, got '%s'", peek)
96+ if peek != "line 6" {
97+ t.Errorf("expected clean peek line 'line 6', got '%s'", peek)
98+ }
99+
100+ lines := extractLastOutputLines(rawHistory, 5)
101+ if len(lines) != 5 {
102+ t.Fatalf("expected 5 lines, got %d", len(lines))
103+ }
104+ if lines[0] != "line 2" || lines[4] != "line 6" {
105+ t.Errorf("unexpected extracted lines: %v", lines)
98106 }
99107
100108 emptyHistory := " \n\n\x1b[0m\n"
......@@ -102,6 +110,10 @@ func TestUI_LiveActivityPeekExtraction(t *testing.T) {
102110 if peekEmpty != "" {
103111 t.Errorf("expected empty peek, got '%s'", peekEmpty)
104112 }
113+ linesEmpty := extractLastOutputLines(emptyHistory, 5)
114+ if linesEmpty != nil {
115+ t.Errorf("expected nil lines, got %v", linesEmpty)
116+ }
105117 }
106118
107119 func TestUI_RenderStateTransitions(t *testing.T) {
......@@ -128,14 +140,23 @@ func TestUI_RenderStateTransitions(t *testing.T) {
128140 ui.Tasks["fmt"].Duration = 400 * time.Millisecond
129141 ui.Tasks["lint"].Status = "running"
130142 ui.Tasks["lint"].Duration = 5400 * time.Millisecond
131- ui.Tasks["lint"].LastOutput = "running golangci-lint on 28 packages..."
143+ ui.Tasks["lint"].RunningOutput = []string{
144+ "fetching dependencies...",
145+ "building ast...",
146+ "running checkers...",
147+ "analyzing packages...",
148+ "running golangci-lint on 28 packages...",
149+ }
132150
133151 render2 := ui.Render(0)
134152 if !strings.Contains(render2, "fmt") || !strings.Contains(render2, "lint") {
135153 t.Errorf("expected State 2 running tasks, got:\n%s", render2)
136154 }
155+ if !strings.Contains(render2, "│ fetching dependencies...") {
156+ t.Errorf("expected vertical bar prefix in running history, got:\n%s", render2)
157+ }
137158 if !strings.Contains(render2, "└─ running golangci-lint on 28 packages...") {
138- t.Errorf("expected live activity peek under lint, got:\n%s", render2)
159+ t.Errorf("expected last line with corner branch under lint, got:\n%s", render2)
139160 }
140161
141162 // State 3: Failure with error preview
......@@ -166,6 +187,46 @@ func TestUI_RenderStateTransitions(t *testing.T) {
166187 if !strings.Contains(summary, "Report: /tmp/pici-artifacts/my-repo/job123/index.html") {
167188 t.Errorf("expected report path in summary, got:\n%s", summary)
168189 }
190+ if !strings.Contains(summary, "cmd/server/main.go:58:2: errcheck: unchecked error") {
191+ t.Errorf("expected error preview box in summary, got:\n%s", summary)
192+ }
193+}
194+
195+func TestUI_RenderSummary_FailureWithErrorPreview(t *testing.T) {
196+ ui := newPipelineUIState("my-repo", "main", "8f2a1b9", "job123", nil)
197+ ui.TaskOrder = []string{"fmt", "lint", "test"}
198+ ui.Tasks["fmt"] = &TaskUIState{Name: "fmt", Status: "success", Duration: 500 * time.Millisecond}
199+ ui.Tasks["lint"] = &TaskUIState{
200+ Name: "lint",
201+ Status: "failed",
202+ ExitCode: "1",
203+ Duration: 1200 * time.Millisecond,
204+ ErrorPreview: []string{
205+ "line 1: syntax error",
206+ "line 2: undefined symbol foo",
207+ },
208+ }
209+ ui.Tasks["test"] = &TaskUIState{
210+ Name: "test",
211+ Status: "failed",
212+ ExitCode: "2",
213+ Duration: 3400 * time.Millisecond,
214+ ErrorPreview: []string{
215+ "--- FAIL: TestSomething",
216+ " main_test.go:42: assertion failed",
217+ },
218+ }
219+
220+ summary := ui.RenderSummary("/artifacts/index.html", 1)
221+ if !strings.Contains(summary, "2 tasks failed") {
222+ t.Errorf("expected 2 tasks failed, got:\n%s", summary)
223+ }
224+ if !strings.Contains(summary, "syntax error") {
225+ t.Errorf("expected lint error preview in summary, got:\n%s", summary)
226+ }
227+ if !strings.Contains(summary, "FAIL: TestSomething") {
228+ t.Errorf("expected test error preview in summary, got:\n%s", summary)
229+ }
169230 }
170231
171232 func TestUI_RenderSummary_SuccessWithFixed(t *testing.T) {
M run.sh
+1, -1
......@@ -8,7 +8,7 @@ while true; do
88 echo "waiting for pici event ..."
99 event=$(ssh pipe sub pici 2>/dev/null)
1010 if [ -n "$event" ]; then
11- echo "$event" | pici runner --wait
11+ echo "$event" | pici runner
1212 else
1313 echo "could not connect to pipe, waiting 5s..."
1414 sleep 5