diff --git a/go.mod b/go.mod index 60084cea71094e919378ccb6c80a1312e06d5c40..91f42f1013c8af3c387e48c6d0f4e3926410852a 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,8 @@ require ( github.com/bmatcuk/doublestar/v4 v4.9.1 github.com/charlievieth/fastwalk v1.0.14 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.1-0.20251023112313-048e47f1399c github.com/charmbracelet/fang v0.4.3 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea @@ -49,7 +50,7 @@ require ( mvdan.cc/sh/v3 v3.12.1-0.20250902163504-3cf4fd5717a5 ) -replace charm.land/fantasy => ../../fantasy/main/ +replace charm.land/fantasy => ../fantasy require ( cloud.google.com/go v0.116.0 // indirect @@ -78,10 +79,9 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251022202715-ec1499142678 // indirect - github.com/charmbracelet/catwalk v0.7.1-0.20251023112313-048e47f1399c github.com/charmbracelet/colorprofile v0.3.2 github.com/charmbracelet/go-genai v0.0.0-20251021165952-9befde14ce97 // indirect - 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-20250904123553-b4e2667e5ad5 github.com/charmbracelet/x/json v0.2.0 // indirect diff --git a/go.sum b/go.sum index 2ca890cd16ab39860ef5c9d95474b8a07fc5e596..5981cabbb202a1d0a01462ccfed7a753e474ed0c 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251022202715-ec1499142678 h1: github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251022202715-ec1499142678/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.1-0.20251023112313-048e47f1399c h1:FVx5lHa+uGmr3GmShsQWhrpmU8RutJm0+To9rU804Ws= github.com/charmbracelet/catwalk v0.7.1-0.20251023112313-048e47f1399c/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= @@ -94,8 +94,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= 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/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/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 a9c75950d94b469cae62495a7251e145d8974fa0..139549a1f2221dc6e76bc40aac28bbe7a731f438 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.AgentCoordinator != nil { queueSize := m.app.AgentCoordinator.QueuedPrompts(m.session.ID) diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 240814cab761701a730ff5754b733c13826c47bd..d6b3528e7b91e8feded25b374b20f9a11dffc067 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -172,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) { diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index 58776a7f223d01d0e9476d843fb32f82926e6476..96afabdd33ed993c249510639c8540fc0b1beeea 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 ef77a051b72b50bcfcaa968202ff6ee3d56fae3e..6eb6155bc2d419ff5ab336efad840d8808950e48 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() @@ -387,7 +387,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 4e3820b4afab6d6e067e0f379d6febee6d008bc6..808e02f4709574572c02902f710149919fa24fab 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 f813862aa927bb3e44c2eb878b302facd01ab400..ddabf8eadf194abb27ede0897782be39ca08ae6c 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 0781b51c54c7c413af2fa0d4aab018d06105a811..e08af49f8bdbb1dc117c01a6ada4ae341b6b65ba 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 1d8a0a854197d3b7ba9e26426bde8a2679f79573..93c1b6498f418c23a17ef0738d5748e25d04a685 100644 --- a/internal/tui/components/completions/completions.go +++ b/internal/tui/components/completions/completions.go @@ -112,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 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 03289032c069ec423d158060e8805b32af3fcc69..72ee6e353932cbb0714042dc325189f564066aaa 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/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 d1b412aaaf2c791ad7a8361dd7adb14a86247eaf..4ba4a1eda6df887acbee62fd18bfbeec0ffae134 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 bfd7a45ad696a1b25c417ea955f063b91830c1d4..995c46606ffc4b91317643e46eece3f835ad6883 100644 --- a/internal/tui/components/dialogs/reasoning/reasoning.go +++ b/internal/tui/components/dialogs/reasoning/reasoning.go @@ -168,7 +168,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 d5c47b01083cdc1becaed9aac4fb8a5d3e9f3b47..b93f8cc3309f66fb957c40e0d6b25419ef51d4e7 100644 --- a/internal/tui/exp/list/filterable.go +++ b/internal/tui/exp/list/filterable.go @@ -9,6 +9,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" ) @@ -116,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 { 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 356acfb8856dc3414c8cddfbcfc4a1d226b87d9f..2882a36679111bb42a9c9710ae20ae9b9e499d8b 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 c25372295b5c906a6511e9d3fc821ea1e1f97661..c90c6bcfc50cf8c7a062f380a28e13c044d04fa0 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -89,8 +89,6 @@ func (a appModel) Init() tea.Cmd { cmd = a.status.Init() cmds = append(cmds, cmd) - cmds = append(cmds, tea.EnableMouseAllMotion) - return tea.Batch(cmds...) } @@ -104,9 +102,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) @@ -234,9 +230,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]: @@ -263,9 +257,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...) @@ -285,9 +277,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) } @@ -307,9 +297,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) } @@ -324,9 +312,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) @@ -362,9 +348,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) } @@ -467,9 +451,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 } } @@ -573,6 +555,9 @@ 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.AgentCoordinator != nil && a.app.AgentCoordinator.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 eb19ad89544b281af2e836f667ac63aaa6414e01..c3ce1dbf7ad94cc89def5e6a11da94b540f7b38e 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 {