From 1c6e9102517b6d2984d5807731b6adb405eaa9ee Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 22 Feb 2026 21:16:48 +0000 Subject: [PATCH] feat(frame): replace raw ANSI runLive with tea.Program Co-Authored-By: Claude Opus 4.6 --- pkg/cli/frame.go | 72 +++++++++++++------------------------------ pkg/cli/frame_test.go | 13 ++++++++ 2 files changed, 34 insertions(+), 51 deletions(-) diff --git a/pkg/cli/frame.go b/pkg/cli/frame.go index 283159be..7f5cdc2b 100644 --- a/pkg/cli/frame.go +++ b/pkg/cli/frame.go @@ -116,6 +116,10 @@ func (f *Frame) Back() bool { // Stop signals the Frame to exit its Run loop. func (f *Frame) Stop() { + if f.program != nil { + f.program.Quit() + return + } select { case <-f.done: default: @@ -123,6 +127,14 @@ func (f *Frame) Stop() { } } +// Send injects a message into the Frame's tea.Program. +// Safe to call before Run() (message is discarded). +func (f *Frame) Send(msg tea.Msg) { + if f.program != nil { + f.program.Send(msg) + } +} + // WithKeyMap sets custom key bindings for Frame navigation. func (f *Frame) WithKeyMap(km KeyMap) *Frame { f.keyMap = km @@ -482,60 +494,18 @@ func (f *Frame) regionSize(r Region, totalW, totalH int) (int, int) { } func (f *Frame) runLive() { - // Enter alt-screen. - fmt.Fprint(f.out, "\033[?1049h") - // Hide cursor. - fmt.Fprint(f.out, "\033[?25l") - - defer func() { - // Show cursor. - fmt.Fprint(f.out, "\033[?25h") - // Leave alt-screen. - fmt.Fprint(f.out, "\033[?1049l") - }() - - ticker := time.NewTicker(80 * time.Millisecond) - defer ticker.Stop() - - for { - f.renderFrame() - - select { - case <-f.done: - return - case <-ticker.C: - } + opts := []tea.ProgramOption{ + tea.WithAltScreen(), + } + if f.out != os.Stdout { + opts = append(opts, tea.WithOutput(f.out)) } -} -func (f *Frame) renderFrame() { - f.mu.Lock() - defer f.mu.Unlock() + p := tea.NewProgram(f, opts...) + f.program = p - w, h := f.termSize() - - // Move to top-left. - fmt.Fprint(f.out, "\033[H") - // Clear screen. - fmt.Fprint(f.out, "\033[2J") - - order := []Region{RegionHeader, RegionLeft, RegionContent, RegionRight, RegionFooter} - for _, r := range order { - if _, exists := f.layout.regions[r]; !exists { - continue - } - m, ok := f.models[r] - if !ok { - continue - } - rw, rh := f.regionSize(r, w, h) - view := m.View(rw, rh) - if view != "" { - fmt.Fprint(f.out, view) - if !strings.HasSuffix(view, "\n") { - fmt.Fprintln(f.out) - } - } + if _, err := p.Run(); err != nil { + Error(err.Error()) } } diff --git a/pkg/cli/frame_test.go b/pkg/cli/frame_test.go index b867dde8..12bf96fb 100644 --- a/pkg/cli/frame_test.go +++ b/pkg/cli/frame_test.go @@ -426,6 +426,19 @@ func TestFrameTeaModel_Good(t *testing.T) { }) } +func TestFrameSend_Good(t *testing.T) { + t.Run("Send is safe before Run", func(t *testing.T) { + f := NewFrame("C") + f.out = &bytes.Buffer{} + f.Content(StaticModel("x")) + + // Should not panic when program is nil + assert.NotPanics(t, func() { + f.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}}) + }) + }) +} + // indexOf returns the position of substr in s, or -1 if not found. func indexOf(s, substr string) int { for i := range len(s) - len(substr) + 1 {