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
cmd/entire/cli/review
Mtui_sink.go+26/-2
Mtui_sink_test.go+26
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