review: bound TUI post-run shutdown · Entire

review: bound TUI post-run shutdown

9ae8793→main

Sessions

ec3ff809b3f8View transcript

Checkout the hand off doc that I just added.Pi·Opus 4.8·3 steps

Changes

2

9 unmodified lines

10
11
12
13
14
15
16
28 unmodified lines

45
46
47
48
49
50
51
52
148 unmodified lines

201
202
203
201
202
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227

9 unmodified lines

\t"context"
\t"io"
\t"sync"
\t"time"

\ttea "charm.land/bubbletea/v2"
\t"golang.org/x/term"
28 unmodified lines

// Compile-time interface check.
var _ reviewtypes.Sink = (*TUISink)(nil)

var tuiPostRunCompleteGrace = 2 * time.Second

// NewTUISink creates a TUISink wired to cancel for Ctrl+C handling. agents is
// the ordered list of agent names that will run; the dashboard pre-renders one
// row per agent so the user sees the full run shape from the first frame.
148 unmodified lines

\tif !ok {
\t\treturn
\t}
\ts.program.Send(postRunCompleteMsg{})
\ts.Wait()

\t// Program.Send can block if Bubble Tea has not entered its event loop yet.
\t// Send from a goroutine and fall back to Kill so a lost post-run quit cannot
\t// leave the CLI stuck on "Finalizing output..." forever.
\tsent := make(chan struct{})
\tgo func() {
\t\ts.program.Send(postRunCompleteMsg{})
\t\tclose(sent)
\t}

\tselect {
\tcase <-s.done:
\t\treturn
\tcase <-sent:
\tcase <-time.After(tuiPostRunCompleteGrace):
\t\ts.program.Kill()
\t}

\tselect {
\tcase <-s.done:
\tcase <-time.After(tuiPostRunCompleteGrace):
\t\ts.program.Kill()
\t}
}

Mcmd/entire/cli/review/tui_sink.go+26/-2

4 unmodified lines

5
6
7
8
9
10
11
71 unmodified lines

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

4 unmodified lines

\t"testing"
\t"time"

\ttea "charm.land/bubbletea/v2"
\treviewtypes "github.com/entireio/cli/cmd/entire/cli/review/types"
)

71 unmodified lines

\t}
}

func TestTUISink_PostRunCompleteDoesNotHangWhenProgramNeverConsumesQuit(t *testing.T) {
\toldGrace := tuiPostRunCompleteGrace
\ttuiPostRunCompleteGrace = 10 * time.Millisecond
\tt.Cleanup(func() { tuiPostRunCompleteGrace = oldGrace })

\tvar buf bytes.Buffer
\tsink := &TUISink{
\t\tprogram: tea.NewProgram(newReviewTUIModel([]string{"agent-a"}, func() {}), tea.WithOutput(&buf), tea.WithInput(bytes.NewReader(nil))),
\t\tstarted: true,
\t\tdone: make(chan struct{}), // deliberately never closed: models a stuck Bubble Tea shutdown.
\t}

\tdone := make(chan struct{})
\tgo func() {
\t\tsink.PostRunComplete()
\t\tclose(done)
\t}

\tselect {
\tcase <-done:
\tcase <-time.After(time.Second):
\t\tt.Fatal("PostRunComplete hung when the TUI did not consume postRunCompleteMsg")
\t}
}

func TestTUISink_WaitBeforeStart_IsNoOp(t *testing.T) {
\t t.Parallel()
\t var buf bytes.Buffer