diff --git a/.goreleaser.yml b/.goreleaser.yml index aabf2f7606462ebb540fd6ebe9efb302a6855e5f..28539bc1681353065ea542a1e4de711a2d425585 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -303,6 +303,7 @@ changelog: - "^docs: update$" - "^test:" - "^test\\(" + - "^v\\d.*" - "merge conflict" - "merge conflict" - Merge branch diff --git a/README.md b/README.md index 6ed20ff1acbf2384d9daaaa184b2feffa939ceaf..435c999d334a71187464670373b00effd23a8e1a 100644 --- a/README.md +++ b/README.md @@ -650,8 +650,8 @@ See the [contributing guide](https://github.com/charmbracelet/crush?tab=contribu We’d love to hear your thoughts on this project. Need help? We gotchu. You can find us on: - [Twitter](https://twitter.com/charmcli) -- [Discord][discord] - [Slack](https://charm.land/slack) +- [Discord][discord] - [The Fediverse](https://mastodon.social/@charmcli) - [Bluesky](https://bsky.app/profile/charm.land) diff --git a/Taskfile.yaml b/Taskfile.yaml index 65a7e2d42fe8dcb307bced7c1fa9a0326b35ccc1..9e0f214fb1f7081ffb90d53e7a62eab120950b0d 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -2,6 +2,10 @@ version: "3" +vars: + VERSION: + sh: git describe --long 2>/dev/null || echo "" + env: CGO_ENABLED: 0 GOEXPERIMENT: greenteagc @@ -30,8 +34,10 @@ tasks: build: desc: Run build + vars: + LDFLAGS: '{{if .VERSION}}-ldflags="-X github.com/charmbracelet/crush/internal/version.Version={{.VERSION}}"{{end}}' cmds: - - go build . + - go build {{.LDFLAGS}} . generates: - crush @@ -59,8 +65,10 @@ tasks: install: desc: Install the application + vars: + LDFLAGS: '{{if .VERSION}}-ldflags="-X github.com/charmbracelet/crush/internal/version.Version={{.VERSION}}"{{end}}' cmds: - - go install -v . + - go install {{.LDFLAGS}} -v . profile:cpu: desc: 10s CPU profile diff --git a/go.mod b/go.mod index 7c1cf2b1b5033a3e4fb404fa0a09c83f77db8367..a8927a4b2fde67a249a81feafa49d5c8fe2c395e 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/charlievieth/fastwalk v1.0.14 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 - github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 + github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 github.com/charmbracelet/catwalk v0.7.0 github.com/charmbracelet/fang v0.4.3 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 @@ -74,7 +74,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/colorprofile v0.3.2 - github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef + github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4 @@ -148,7 +148,7 @@ require ( golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.37.0 // indirect golang.org/x/term v0.35.0 // indirect golang.org/x/text v0.30.0 golang.org/x/time v0.8.0 // indirect diff --git a/go.sum b/go.sum index bbb776c23ee5a38737e960af5f17764ea977aaab..1d3160e24bcaf521b52c99e300e3ba221be4d656 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf h1: github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 h1:973OHYuq2Jx9deyuPwe/6lsuQrDCatOsjP8uCd02URE= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 h1:hjOhtqsxa+LVuCAkzhfA43wtusOaUPyQdSTg/wbRscw= -github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619/go.mod h1:5IzIGXU1n0foRc8bRAherC8ZuQCQURPlwx3ANLq1138= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 h1:oAChAeh730gtLKK/BpaTeJHzmj3KFuEfQ7AZgf2VGHM= +github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5/go.mod h1:SUTLq+/pGQ5qntHgt0JswfVJFfgJgWDqyvyiSLVlmbo= github.com/charmbracelet/catwalk v0.7.0 h1:qhLv56aeel5Q+2G/YFh9k5FhTqsozsn4HYViuAQ/Rio= github.com/charmbracelet/catwalk v0.7.0/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= @@ -92,8 +92,8 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M= -github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o= +github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 h1:Lr+igmzKpLPdb8yUZBP9noYWwCZP042z2nWPrJZTc+8= +github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731/go.mod h1:KfWwUa0Oe//D72YlhbOq/g40L7UiGtATrvsGI3cciG8= github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a h1:zYSNtEJM9jwHbJts2k+Hroj+xQwsW1yxc4Wopdv7KaI= @@ -388,8 +388,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/cmd/logs.go b/internal/cmd/logs.go index e7160f4a1307406be20f1fe00a59e93de5232d67..4372083189701e1410c83690c18fbd371f778169 100644 --- a/internal/cmd/logs.go +++ b/internal/cmd/logs.go @@ -10,8 +10,10 @@ import ( "slices" "time" + "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/log/v2" + "github.com/charmbracelet/x/term" "github.com/nxadm/tail" "github.com/spf13/cobra" ) @@ -45,6 +47,9 @@ var logsCmd = &cobra.Command{ log.SetLevel(log.DebugLevel) log.SetOutput(os.Stdout) + if !term.IsTerminal(os.Stdout.Fd()) { + log.SetColorProfile(colorprofile.NoTTY) + } cfg, err := config.Load(cwd, dataDir, false) if err != nil { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index d6a26d818643a05704f554223a7b7960792970c5..005f2e86f7012b265fb619580c7cc2eec2e4de03 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -83,11 +83,8 @@ crush -y // Set up the TUI. program := tea.NewProgram( tui.New(app), - tea.WithAltScreen(), tea.WithContext(cmd.Context()), - tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding - tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state - ) + tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state go app.Subscribe(program) diff --git a/internal/config/provider.go b/internal/config/provider.go index 671c348f71da3a79f65c14c624bdaf2adc011411..108d6a667794e4f8f1beebe6997848a65d8fd6e6 100644 --- a/internal/config/provider.go +++ b/internal/config/provider.go @@ -126,7 +126,7 @@ func Providers(cfg *Config) ([]catwalk.Provider, error) { } func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) ([]catwalk.Provider, error) { - cacheIsStale, cacheExists := isCacheStale(path) + _, cacheExists := isCacheStale(path) catwalkGetAndSave := func() ([]catwalk.Provider, error) { providers, err := client.GetProviders() @@ -142,25 +142,6 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) return providers, nil } - backgroundCacheUpdate := func() { - go func() { - slog.Info("Updating providers cache in background", "path", path) - - providers, err := client.GetProviders() - if err != nil { - slog.Error("Failed to fetch providers in background from Catwalk", "error", err) - return - } - if len(providers) == 0 { - slog.Error("Empty providers list from Catwalk") - return - } - if err := saveProvidersInCache(path, providers); err != nil { - slog.Error("Failed to update providers.json in background", "error", err) - } - }() - } - switch { case autoUpdateDisabled: slog.Warn("Providers auto-update is disabled") @@ -177,19 +158,6 @@ func loadProviders(autoUpdateDisabled bool, client ProviderClient, path string) } return providers, nil - case cacheExists && !cacheIsStale: - slog.Info("Recent providers cache is available.", "path", path) - - providers, err := loadProvidersFromCache(path) - if err != nil { - return nil, err - } - if len(providers) == 0 { - return catwalkGetAndSave() - } - backgroundCacheUpdate() - return providers, nil - default: slog.Info("Cache is not available or is stale. Fetching providers from Catwalk.", "path", path) diff --git a/internal/config/provider_test.go b/internal/config/provider_test.go index 8b499919bca666915a89d38c1e5014a911f4d2d1..1262b60ef42050b9061c9f7c6be4dc431efe3548 100644 --- a/internal/config/provider_test.go +++ b/internal/config/provider_test.go @@ -57,7 +57,7 @@ func TestProvider_loadProvidersWithIssues(t *testing.T) { if err != nil { t.Fatalf("Failed to write old providers to file: %v", err) } - providers, err := loadProviders(false, client, tmpPath) + providers, err := loadProviders(true, client, tmpPath) require.NoError(t, err) require.NotNil(t, providers) require.Len(t, providers, 1) diff --git a/internal/format/spinner.go b/internal/format/spinner.go index 69e443d0f67adadd1e3f9b9a13129850324b6184..d1d9805f4b7bd511270316c4b1f0dafdfe9401b3 100644 --- a/internal/format/spinner.go +++ b/internal/format/spinner.go @@ -23,8 +23,8 @@ type model struct { anim *anim.Anim } -func (m model) Init() tea.Cmd { return m.anim.Init() } -func (m model) View() string { return m.anim.View() } +func (m model) Init() tea.Cmd { return m.anim.Init() } +func (m model) View() tea.View { return tea.NewView(m.anim.View()) } // Update implements tea.Model. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/internal/log/http.go b/internal/log/http.go index 46c4b42af599f1809478a5c3f083c6249a3e13d0..a4564ffdc50335e3944c44ccf0a9a562e2f6454a 100644 --- a/internal/log/http.go +++ b/internal/log/http.go @@ -39,12 +39,14 @@ func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, erro return nil, err } - slog.Debug( - "HTTP Request", - "method", req.Method, - "url", req.URL, - "body", bodyToString(save), - ) + if slog.Default().Enabled(req.Context(), slog.LevelDebug) { + slog.Debug( + "HTTP Request", + "method", req.Method, + "url", req.URL, + "body", bodyToString(save), + ) + } start := time.Now() resp, err := h.Transport.RoundTrip(req) @@ -61,16 +63,18 @@ func (h *HTTPRoundTripLogger) RoundTrip(req *http.Request) (*http.Response, erro } save, resp.Body, err = drainBody(resp.Body) - slog.Debug( - "HTTP Response", - "status_code", resp.StatusCode, - "status", resp.Status, - "headers", formatHeaders(resp.Header), - "body", bodyToString(save), - "content_length", resp.ContentLength, - "duration_ms", duration.Milliseconds(), - "error", err, - ) + if slog.Default().Enabled(req.Context(), slog.LevelDebug) { + slog.Debug( + "HTTP Response", + "status_code", resp.StatusCode, + "status", resp.Status, + "headers", formatHeaders(resp.Header), + "body", bodyToString(save), + "content_length", resp.ContentLength, + "duration_ms", duration.Milliseconds(), + "error", err, + ) + } return resp, err } @@ -84,7 +88,7 @@ func bodyToString(body io.ReadCloser) string { return "" } var b bytes.Buffer - if json.Compact(&b, bytes.TrimSpace(src)) != nil { + if json.Indent(&b, bytes.TrimSpace(src), "", " ") != nil { // not json probably return string(src) } diff --git a/internal/tui/components/anim/anim.go b/internal/tui/components/anim/anim.go index 05ac4da98281248d1774a10e95f4d8e2f177e048..d04176ba9e07c2ce15427e9496cf0896222ba930 100644 --- a/internal/tui/components/anim/anim.go +++ b/internal/tui/components/anim/anim.go @@ -16,6 +16,7 @@ import ( "github.com/lucasb-eyer/go-colorful" "github.com/charmbracelet/crush/internal/csync" + "github.com/charmbracelet/crush/internal/tui/util" ) const ( @@ -318,7 +319,7 @@ func (a *Anim) Init() tea.Cmd { } // Update processes animation steps (or not). -func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (a *Anim) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case StepMsg: if msg.id != a.id { diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 8688f7e24c94290c74ae4344499acff61b43ac39..aaaf683494a8dd1608d9ebae4f07dae6037def26 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -101,7 +101,7 @@ func (m *messageListCmp) Init() tea.Cmd { } // Update handles incoming messages and updates the component state. -func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd if m.session.ID != "" && m.app.CoderAgent != nil { queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index f70a0a3dbe63a9473f552efa233e03bd4efc0ee1..92c6bea70c3e43af1b92f03c30ba3e15af0f5e4d 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -86,6 +86,7 @@ var DeleteKeyMaps = DeleteAttachmentKeyMaps{ const ( maxAttachments = 5 + maxFileResults = 25 ) type OpenEditorMsg struct { @@ -171,7 +172,7 @@ func (m *editorCmp) repositionCompletions() tea.Msg { return completions.RepositionCompletionsMsg{X: x, Y: y} } -func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { @@ -500,6 +501,7 @@ func (m *editorCmp) startCompletions() tea.Msg { Completions: completionItems, X: x, Y: y, + MaxResults: maxFileResults, } } diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index 21861a4a2eda1340f6e01c0748f24cb713f15398..6bea86690b4ffe813799ad6e1ba01359562ed791 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -44,7 +44,7 @@ func (h *header) Init() tea.Cmd { return nil } -func (h *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (h *header) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case pubsub.Event[session.Session]: if msg.Type == pubsub.UpdatedEvent { diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index d931ba7e179255d6639db78ebea5e82b57af1504..eb0e9e84c3cd1342126b8de9acef6d145a15bb62 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -35,7 +35,7 @@ var ClearSelectionKey = key.NewBinding(key.WithKeys("esc", "alt+esc"), key.WithH // MessageCmp defines the interface for message components in the chat interface. // It combines standard UI model interfaces with message-specific functionality. type MessageCmp interface { - util.Model // Basic Bubble Tea model interface + util.Model // Basic Bubble util.Model interface layout.Sizeable // Width/height management layout.Focusable // Focus state management GetMessage() message.Message // Access to underlying message data @@ -94,7 +94,7 @@ func (m *messageCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. // Manages animation updates for spinning messages and stops animation when appropriate. -func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *messageCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case anim.StepMsg: m.spinning = m.shouldSpin() @@ -384,7 +384,7 @@ func (m *assistantSectionModel) Init() tea.Cmd { return nil } -func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (m *assistantSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) { return m, nil } diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 7e03674f97243e7d9e569b341fe1c6f1d2450b93..1899a47ec14ab185f06f64063223a0fc5d24826f 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -27,7 +27,7 @@ import ( // ToolCallCmp defines the interface for tool call components in the chat interface. // It manages the display of tool execution including pending states, results, and errors. type ToolCallCmp interface { - util.Model // Basic Bubble Tea model interface + util.Model // Basic Bubble util.Model interface layout.Sizeable // Width/height management layout.Focusable // Focus state management GetToolCall() message.ToolCall // Access to tool call data @@ -147,7 +147,7 @@ func (m *toolCallCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. // Manages animation updates for pending tool calls. -func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case anim.StepMsg: var cmds []tea.Cmd @@ -160,7 +160,7 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.spinning { u, cmd := m.anim.Update(msg) - m.anim = u.(util.Model) + m.anim = u cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index b50a78c7f8697e4f4db19649a01794cfe7a23bac..28808e0a8e57df881263d2fb90d25dfe8d02b83b 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -88,7 +88,7 @@ func (m *sidebarCmp) Init() tea.Cmd { return nil } -func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *sidebarCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case SessionFilesMsg: m.files = csync.NewMap[string, SessionFile]() diff --git a/internal/tui/components/chat/splash/splash.go b/internal/tui/components/chat/splash/splash.go index 187fc35e6ec47a858b99f35e135a8cef3500fbf1..e6446a981754665ba32beca48dc4a395addc5b93 100644 --- a/internal/tui/components/chat/splash/splash.go +++ b/internal/tui/components/chat/splash/splash.go @@ -135,7 +135,7 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd { } // Update implements SplashPage. -func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: return s, s.SetSize(msg.Width, msg.Height) diff --git a/internal/tui/components/completions/completions.go b/internal/tui/components/completions/completions.go index ae3c233e4f21b089f59b7effb88ddc3300277d16..93c1b6498f418c23a17ef0738d5748e25d04a685 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -22,6 +22,7 @@ type OpenCompletionsMsg struct { Completions []Completion X int // X position for the completions popup Y int // Y position for the completions popup + MaxResults int // Maximum number of results to render, 0 for no limit } type FilterCompletionsMsg struct { @@ -111,7 +112,7 @@ func (c *completionsCmp) Init() tea.Cmd { } // Update implements Completions. -func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *completionsCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth, c.wHeight = msg.Width, msg.Height @@ -192,6 +193,7 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } c.width = width c.height = max(min(maxCompletionsHeight, len(items)), 1) // Ensure at least 1 item height + c.list.SetResultsSize(msg.MaxResults) return c, tea.Batch( c.list.SetItems(items), c.list.SetSize(c.width, c.height), diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index b01873a22b18f87d798757bb5a6ba799ae0e7a81..effbaac9d48c8600c2b9b0e7dce94b9bbf5b429b 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -36,7 +36,7 @@ func (m *statusCmp) Init() tea.Cmd { return nil } -func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *statusCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 72677bc934864970c2cbded87b31853ad702a6ed..66ad3f7ba06ae41fa2a4d0e033906ceda5298c22 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -92,7 +92,7 @@ func (c *commandArgumentsDialogCmp) Init() tea.Cmd { } // Update implements CommandArgumentsDialog. -func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/commands/commands.go b/internal/tui/components/dialogs/commands/commands.go index 664158fc392a87d8a7725bfa964748f7ef4f8e67..d05ec8fea44415ab83158849319cde62f96ef329 100644 --- a/internal/tui/components/dialogs/commands/commands.go +++ b/internal/tui/components/dialogs/commands/commands.go @@ -116,7 +116,7 @@ func (c *commandDialogCmp) Init() tea.Cmd { return c.SetCommandType(c.commandType) } -func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/compact/compact.go b/internal/tui/components/dialogs/compact/compact.go index ecde402fd8dfe1f31791834cd4e4bae13ec45e00..12cea72b55b4b3ad4f11c2f756ad7961ba3c8f87 100644 --- a/internal/tui/components/dialogs/compact/compact.go +++ b/internal/tui/components/dialogs/compact/compact.go @@ -61,7 +61,7 @@ func (c *compactDialogCmp) Init() tea.Cmd { return nil } -func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (c *compactDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: c.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/dialogs.go b/internal/tui/components/dialogs/dialogs.go index 99e14e51fdd271a9cee0c27528c7608ea28fa24e..d5ad83c160e0e618e637dabe2b5e297ff0c1cd65 100644 --- a/internal/tui/components/dialogs/dialogs.go +++ b/internal/tui/components/dialogs/dialogs.go @@ -32,7 +32,7 @@ type CloseDialogMsg struct{} // DialogCmp manages a stack of dialogs with keyboard navigation. type DialogCmp interface { - tea.Model + util.Model Dialogs() []DialogModel HasDialogs() bool @@ -62,7 +62,7 @@ func (d dialogCmp) Init() tea.Cmd { } // Update handles dialog lifecycle and forwards messages to the active dialog. -func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (d dialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: var cmds []tea.Cmd @@ -98,7 +98,11 @@ func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return d, nil } -func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) { +func (d dialogCmp) View() string { + return "" +} + +func (d dialogCmp) handleOpen(msg OpenDialogMsg) (util.Model, tea.Cmd) { if d.HasDialogs() { dialog := d.dialogs[len(d.dialogs)-1] if dialog.ID() == msg.Model.ID() { diff --git a/internal/tui/components/dialogs/filepicker/filepicker.go b/internal/tui/components/dialogs/filepicker/filepicker.go index fcec2fc8b6e3e606e555c55949049f397a30f921..85a391ce5ceba7689148fbdcd016b73c1e100f54 100644 --- a/internal/tui/components/dialogs/filepicker/filepicker.go +++ b/internal/tui/components/dialogs/filepicker/filepicker.go @@ -88,7 +88,7 @@ func (m *model) Init() tea.Cmd { return m.filePicker.Init() } -func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *model) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/models/apikey.go b/internal/tui/components/dialogs/models/apikey.go index 0490335f9ad745839a94de0460a0fc5c1b6f125c..1c4ee0c14a77e2006d2bd43e40947b6852fa1736 100644 --- a/internal/tui/components/dialogs/models/apikey.go +++ b/internal/tui/components/dialogs/models/apikey.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/home" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" ) @@ -75,7 +76,7 @@ func (a *APIKeyInput) Init() tea.Cmd { return a.spinner.Tick } -func (a *APIKeyInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (a *APIKeyInput) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case spinner.TickMsg: if a.state == APIKeyInputStateVerifying { diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index 7c2863706c29180cffcfb88c385a012e39df464c..2e0b68cc3640c9ee5ed411eb10a07e9dc3bc0635 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -98,7 +98,7 @@ func (m *modelDialogCmp) Init() tea.Cmd { return tea.Batch(m.modelList.Init(), m.apiKeyInput.Init()) } -func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index 9e0a6b05d7385c354f8faba3110b1c0951f9a97d..7705edd394bd91466220326c474ea2b8ef55ffc7 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/internal/tui/components/dialogs/permissions/permissions.go @@ -95,7 +95,7 @@ func (p *permissionDialogCmp) supportsDiffView() bool { return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName } -func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (p *permissionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { diff --git a/internal/tui/components/dialogs/quit/quit.go b/internal/tui/components/dialogs/quit/quit.go index 763dc842d386a072176e1a26741d8b68c1e2993b..a8857104550886abc5f70956bf384ab2df6ec302 100644 --- a/internal/tui/components/dialogs/quit/quit.go +++ b/internal/tui/components/dialogs/quit/quit.go @@ -40,7 +40,7 @@ func (q *quitDialogCmp) Init() tea.Cmd { } // Update handles keyboard input for the quit dialog. -func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (q *quitDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: q.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/reasoning/reasoning.go b/internal/tui/components/dialogs/reasoning/reasoning.go index ba49abd8c58a0e7eb84235e7b68f5f5193a96b1b..81f521c4bd31daa25fad5ccfb127a80ea2f20eba 100644 --- a/internal/tui/components/dialogs/reasoning/reasoning.go +++ b/internal/tui/components/dialogs/reasoning/reasoning.go @@ -172,7 +172,7 @@ func (r *reasoningDialogCmp) populateEffortOptions() tea.Cmd { return nil } -func (r *reasoningDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (r *reasoningDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: r.wWidth = msg.Width diff --git a/internal/tui/components/dialogs/sessions/sessions.go b/internal/tui/components/dialogs/sessions/sessions.go index 037eb5ebb727a24b8ab9bfda2e2c72943120e819..7f01f3ba4dacfe408fed0e8f5a2f34b39d8b2edd 100644 --- a/internal/tui/components/dialogs/sessions/sessions.go +++ b/internal/tui/components/dialogs/sessions/sessions.go @@ -81,7 +81,7 @@ func (s *sessionDialogCmp) Init() tea.Cmd { return tea.Sequence(cmds...) } -func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: var cmds []tea.Cmd diff --git a/internal/tui/exp/list/filterable.go b/internal/tui/exp/list/filterable.go index e639786db5777aaeda237e959dffe36d9c6a7583..b93f8cc3309f66fb957c40e0d6b25419ef51d4e7 100644 --- a/internal/tui/exp/list/filterable.go +++ b/internal/tui/exp/list/filterable.go @@ -3,14 +3,13 @@ package list import ( "regexp" "slices" - "sort" - "strings" "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/textinput" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/sahilm/fuzzy" ) @@ -28,7 +27,9 @@ type FilterableList[T FilterableItem] interface { Cursor() *tea.Cursor SetInputWidth(int) SetInputPlaceholder(string) + SetResultsSize(int) Filter(q string) tea.Cmd + fuzzy.Source } type HasMatchIndexes interface { @@ -47,10 +48,11 @@ type filterableList[T FilterableItem] struct { *filterableOptions width, height int // stores all available items - items []T - input textinput.Model - inputWidth int - query string + items []T + resultsSize int + input textinput.Model + inputWidth int + query string } type filterableListOption func(*filterableOptions) @@ -115,7 +117,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption return f } -func (f *filterableList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *filterableList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { @@ -246,22 +248,18 @@ func (f *filterableList[T]) Filter(query string) tea.Cmd { return f.list.SetItems(f.items) } - words := make([]string, len(f.items)) - for i, item := range f.items { - words[i] = strings.ToLower(item.FilterValue()) - } - - matches := fuzzy.Find(query, words) - - sort.SliceStable(matches, func(i, j int) bool { - return matches[i].Score > matches[j].Score - }) + matches := fuzzy.FindFrom(query, f) var matchedItems []T - for _, match := range matches { + resultSize := len(matches) + if f.resultsSize > 0 && resultSize > f.resultsSize { + resultSize = f.resultsSize + } + for i := range resultSize { + match := matches[i] item := f.items[match.Index] - if i, ok := any(item).(HasMatchIndexes); ok { - i.MatchIndexes(match.MatchedIndexes) + if it, ok := any(item).(HasMatchIndexes); ok { + it.MatchIndexes(match.MatchedIndexes) } matchedItems = append(matchedItems, item) } @@ -307,3 +305,15 @@ func (f *filterableList[T]) SetInputWidth(w int) { func (f *filterableList[T]) SetInputPlaceholder(ph string) { f.placeholder = ph } + +func (f *filterableList[T]) SetResultsSize(size int) { + f.resultsSize = size +} + +func (f *filterableList[T]) String(i int) string { + return f.items[i].FilterValue() +} + +func (f *filterableList[T]) Len() int { + return len(f.items) +} diff --git a/internal/tui/exp/list/filterable_group.go b/internal/tui/exp/list/filterable_group.go index 6e9a5dc7eaad66d32ec34baf7e41d35ab3233048..15084cce28be5190367eba861a491231139af53f 100644 --- a/internal/tui/exp/list/filterable_group.go +++ b/internal/tui/exp/list/filterable_group.go @@ -11,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/sahilm/fuzzy" ) @@ -65,7 +66,7 @@ func NewFilterableGroupedList[T FilterableItem](items []Group[T], opts ...filter return f } -func (f *filterableGroupList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (f *filterableGroupList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch { diff --git a/internal/tui/exp/list/grouped.go b/internal/tui/exp/list/grouped.go index cb54628a70e84cb80eeb162a0d9f836f14271641..43223602dbfbeaa0ae60d0368b95a4f455228f96 100644 --- a/internal/tui/exp/list/grouped.go +++ b/internal/tui/exp/list/grouped.go @@ -58,7 +58,7 @@ func (g *groupedList[T]) Init() tea.Cmd { return g.render() } -func (l *groupedList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (l *groupedList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { u, cmd := l.list.Update(msg) l.list = u.(*list[Item]) return l, cmd diff --git a/internal/tui/exp/list/items.go b/internal/tui/exp/list/items.go index 9e7259dc10a61c95e970d9f1fc93b0d61d7a65a8..143908d5416be744424cc30965b8d663ca2a2c68 100644 --- a/internal/tui/exp/list/items.go +++ b/internal/tui/exp/list/items.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/styles" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/ansi" "github.com/google/uuid" @@ -97,7 +98,7 @@ func (c *completionItemCmp[T]) Init() tea.Cmd { } // Update implements CommandItem. -func (c *completionItemCmp[T]) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (c *completionItemCmp[T]) Update(tea.Msg) (util.Model, tea.Cmd) { return c, nil } @@ -348,7 +349,7 @@ func (m *itemSectionModel) Init() tea.Cmd { return nil } -func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { +func (m *itemSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) { return m, nil } diff --git a/internal/tui/exp/list/list.go b/internal/tui/exp/list/list.go index e18b88348959c59190f1741698f76c33f04571db..ea04b0c7d640f7801ba320d26071e21eafbbd90c 100644 --- a/internal/tui/exp/list/list.go +++ b/internal/tui/exp/list/list.go @@ -217,7 +217,7 @@ func (l *list[T]) Init() tea.Cmd { } // Update implements List. -func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (l *list[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) { switch msg := msg.(type) { case tea.MouseWheelMsg: if l.enableMouse { @@ -277,7 +277,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return l, nil } -func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) { +func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (util.Model, tea.Cmd) { var cmd tea.Cmd switch msg.Button { case tea.MouseWheelDown: diff --git a/internal/tui/exp/list/list_test.go b/internal/tui/exp/list/list_test.go index 63cfa599e8ce1c96aad1cae67243caa2b097ee0b..4e6d8e3110d8c585b26293b7ef1f1e80e06c8b50 100644 --- a/internal/tui/exp/list/list_test.go +++ b/internal/tui/exp/list/list_test.go @@ -7,6 +7,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/exp/golden" "github.com/google/uuid" @@ -602,7 +603,7 @@ func (s *simpleItem) Init() tea.Cmd { return nil } -func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (s *simpleItem) Update(msg tea.Msg) (util.Model, tea.Cmd) { return s, nil } @@ -644,7 +645,7 @@ func (s *selectableItem) IsFocused() bool { return s.focused } -func execCmd(m tea.Model, cmd tea.Cmd) { +func execCmd(m util.Model, cmd tea.Cmd) { for cmd != nil { msg := cmd() m, cmd = m.Update(msg) diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 2918925068cb2f012bead47bbf44260c6255288c..1559d314d052d118019797f85eedd91a7e0f6d00 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -163,7 +163,7 @@ func (p *chatPage) Init() tea.Cmd { ) } -func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.KeyboardEnhancementsMsg: diff --git a/internal/tui/tui.go b/internal/tui/tui.go index f2873dcb8ffbf485cba2c90743940dccb7931951..38091ff4561dec2a741e3d8ded5b30d06e3493e7 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -91,8 +91,6 @@ func (a appModel) Init() tea.Cmd { cmd = a.status.Init() cmds = append(cmds, cmd) - cmds = append(cmds, tea.EnableMouseAllMotion) - return tea.Batch(cmds...) } @@ -106,9 +104,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyboardEnhancementsMsg: for id, page := range a.pages { m, pageCmd := page.Update(msg) - if model, ok := m.(util.Model); ok { - a.pages[id] = model - } + a.pages[id] = m if pageCmd != nil { cmds = append(cmds, pageCmd) @@ -232,9 +228,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Forward to view. updated, itemCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated return a, itemCmd case pubsub.Event[permission.PermissionRequest]: @@ -303,9 +297,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.isConfigured = config.HasInitialDataConfig() updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) return a, tea.Batch(cmds...) @@ -325,9 +317,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) } @@ -347,9 +337,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, pageCmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated cmds = append(cmds, pageCmd) } @@ -364,9 +352,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } updated, cmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated if a.dialog.HasDialogs() { u, dialogCmd := a.dialog.Update(msg) @@ -402,9 +388,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd { // Update the current view. for p, page := range a.pages { updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height}) - if model, ok := updated.(util.Model); ok { - a.pages[p] = model - } + a.pages[p] = updated cmds = append(cmds, pageCmd) } @@ -507,9 +491,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { } updated, cmd := item.Update(msg) - if model, ok := updated.(util.Model); ok { - a.pages[a.currentPage] = model - } + a.pages[a.currentPage] = updated return cmd } } @@ -613,6 +595,8 @@ func (a *appModel) View() tea.View { view.Layer = canvas view.Cursor = cursor + view.MouseMode = tea.MouseModeCellMotion + view.AltScreen = true if a.app != nil && a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() { // HACK: use a random percentage to prevent ghostty from hiding it // after a timeout. diff --git a/internal/tui/util/util.go b/internal/tui/util/util.go index 37e35b85c198e932f24c509a161f83bf82de335e..fef618b394895907b670e557181a4b0c07a084f8 100644 --- a/internal/tui/util/util.go +++ b/internal/tui/util/util.go @@ -12,8 +12,9 @@ type Cursor interface { } type Model interface { - tea.Model - tea.ViewModel + Init() tea.Cmd + Update(tea.Msg) (Model, tea.Cmd) + View() string } func CmdHandler(msg tea.Msg) tea.Cmd {