on main help sign in
refactor: use v2 declarative API (#1229)
14f2f9cAyman Bagabas
go.mod+3−3
@@ -13,7 +13,7 @@ require (
13 github.com/charlievieth/fastwalk v1.0.14
13 github.com/charlievieth/fastwalk v1.0.14
14 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf
14 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf
15 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2
15 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2
16− github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619
16+ github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5
17 github.com/charmbracelet/catwalk v0.7.0
17 github.com/charmbracelet/catwalk v0.7.0
18 github.com/charmbracelet/fang v0.4.3
18 github.com/charmbracelet/fang v0.4.3
19 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
19 github.com/charmbracelet/glamour/v2 v2.0.0-20250811143442-a27abb32f018
@@ -74,7 +74,7 @@ require (
74 github.com/bahlo/generic-list-go v0.2.0 // indirect
74 github.com/bahlo/generic-list-go v0.2.0 // indirect
75 github.com/buger/jsonparser v1.1.1 // indirect
75 github.com/buger/jsonparser v1.1.1 // indirect
76 github.com/charmbracelet/colorprofile v0.3.2
76 github.com/charmbracelet/colorprofile v0.3.2
77− github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef
77+ github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731
78 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect
78 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a // indirect
79 github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d
79 github.com/charmbracelet/x/exp/slice v0.0.0-20250829135019-44e44e21330d
80 github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4
80 github.com/charmbracelet/x/powernap v0.0.0-20251015113943-25f979b54ad4
@@ -148,7 +148,7 @@ require (
148 golang.org/x/net v0.43.0 // indirect
148 golang.org/x/net v0.43.0 // indirect
149 golang.org/x/oauth2 v0.30.0 // indirect
149 golang.org/x/oauth2 v0.30.0 // indirect
150 golang.org/x/sync v0.17.0 // indirect
150 golang.org/x/sync v0.17.0 // indirect
151− golang.org/x/sys v0.36.0 // indirect
151+ golang.org/x/sys v0.37.0 // indirect
152 golang.org/x/term v0.35.0 // indirect
152 golang.org/x/term v0.35.0 // indirect
153 golang.org/x/text v0.30.0
153 golang.org/x/text v0.30.0
154 golang.org/x/time v0.8.0 // indirect
154 golang.org/x/time v0.8.0 // indirect
go.sum+6−6
@@ -78,8 +78,8 @@ github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf h1:
78github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4=
78github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251021163913-d29170d047bf/go.mod h1:8TIYxZxsuCqqeJ0lga/b91tBwrbjoHDC66Sq5t8N2R4=
79github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 h1:973OHYuq2Jx9deyuPwe/6lsuQrDCatOsjP8uCd02URE=
79github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2 h1:973OHYuq2Jx9deyuPwe/6lsuQrDCatOsjP8uCd02URE=
80github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
80github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250820203609-601216f68ee2/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
81−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619 h1:hjOhtqsxa+LVuCAkzhfA43wtusOaUPyQdSTg/wbRscw=
81+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5 h1:oAChAeh730gtLKK/BpaTeJHzmj3KFuEfQ7AZgf2VGHM=
82−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4.0.20251011205917-3b687ffc1619/go.mod h1:5IzIGXU1n0foRc8bRAherC8ZuQCQURPlwx3ANLq1138=
82+github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.5/go.mod h1:SUTLq+/pGQ5qntHgt0JswfVJFfgJgWDqyvyiSLVlmbo=
83github.com/charmbracelet/catwalk v0.7.0 h1:qhLv56aeel5Q+2G/YFh9k5FhTqsozsn4HYViuAQ/Rio=
83github.com/charmbracelet/catwalk v0.7.0 h1:qhLv56aeel5Q+2G/YFh9k5FhTqsozsn4HYViuAQ/Rio=
84github.com/charmbracelet/catwalk v0.7.0/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY=
84github.com/charmbracelet/catwalk v0.7.0/go.mod h1:ReU4SdrLfe63jkEjWMdX2wlZMV3k9r11oQAmzN0m+KY=
85github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
85github.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
92github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM=
92github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250917201909-41ff0bf215ea/go.mod h1:ngHerf1JLJXBrDXdphn5gFrBPriCL437uwukd5c93pM=
93github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
93github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
94github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
94github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
95−github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef h1:VrWaUi2LXYLjfjCHowdSOEc6dQ9Ro14KY7Bw4IWd19M=
95+github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731 h1:Lr+igmzKpLPdb8yUZBP9noYWwCZP042z2nWPrJZTc+8=
96−github.com/charmbracelet/ultraviolet v0.0.0-20250915111650-81d4262876ef/go.mod h1:AThRsQH1t+dfyOKIwXRoJBniYFQUkUpQq4paheHMc2o=
96+github.com/charmbracelet/ultraviolet v0.0.0-20251017140847-d4ace4d6e731/go.mod h1:KfWwUa0Oe//D72YlhbOq/g40L7UiGtATrvsGI3cciG8=
97github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
97github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
98github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
98github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
99github.com/charmbracelet/x/cellbuf v0.0.14-0.20250811133356-e0c5dbe5ea4a h1:zYSNtEJM9jwHbJts2k+Hroj+xQwsW1yxc4Wopdv7KaI=
99github.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=
388golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
388golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
389golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
389golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
390golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
390golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
391−golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
391+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
392−golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
392+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
393golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
393golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
394golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
394golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
395golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
395golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
internal/cmd/root.go+1−4
@@ -83,11 +83,8 @@ crush -y
83 // Set up the TUI.
83 // Set up the TUI.
84 program := tea.NewProgram(
84 program := tea.NewProgram(
85 tui.New(app),
85 tui.New(app),
86− tea.WithAltScreen(),
87 tea.WithContext(cmd.Context()),
86 tea.WithContext(cmd.Context()),
88− tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
89− tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
87+ tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state
90− )
91
88
92 go app.Subscribe(program)
89 go app.Subscribe(program)
93
90
internal/format/spinner.go+2−2
@@ -23,8 +23,8 @@ type model struct {
23 anim *anim.Anim
23 anim *anim.Anim
24}
24}
25
25
26−func (m model) Init() tea.Cmd { return m.anim.Init() }
26+func (m model) Init() tea.Cmd { return m.anim.Init() }
27−func (m model) View() string { return m.anim.View() }
27+func (m model) View() tea.View { return tea.NewView(m.anim.View()) }
28
28
29// Update implements tea.Model.
29// Update implements tea.Model.
30func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
30func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
internal/tui/components/anim/anim.go+2−1
@@ -16,6 +16,7 @@ import (
16 "github.com/lucasb-eyer/go-colorful"
16 "github.com/lucasb-eyer/go-colorful"
17
17
18 "github.com/charmbracelet/crush/internal/csync"
18 "github.com/charmbracelet/crush/internal/csync"
19+ "github.com/charmbracelet/crush/internal/tui/util"
19)
20)
20
21
21const (
22const (
@@ -318,7 +319,7 @@ func (a *Anim) Init() tea.Cmd {
318}
319}
319
320
320// Update processes animation steps (or not).
321// Update processes animation steps (or not).
321−func (a *Anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
322+func (a *Anim) Update(msg tea.Msg) (util.Model, tea.Cmd) {
322 switch msg := msg.(type) {
323 switch msg := msg.(type) {
323 case StepMsg:
324 case StepMsg:
324 if msg.id != a.id {
325 if msg.id != a.id {
internal/tui/components/chat/chat.go+1−1
@@ -101,7 +101,7 @@ func (m *messageListCmp) Init() tea.Cmd {
101}
101}
102
102
103// Update handles incoming messages and updates the component state.
103// Update handles incoming messages and updates the component state.
104−func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
104+func (m *messageListCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
105 var cmds []tea.Cmd
105 var cmds []tea.Cmd
106 if m.session.ID != "" && m.app.CoderAgent != nil {
106 if m.session.ID != "" && m.app.CoderAgent != nil {
107 queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID)
107 queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID)
internal/tui/components/chat/editor/editor.go+1−1
@@ -172,7 +172,7 @@ func (m *editorCmp) repositionCompletions() tea.Msg {
172 return completions.RepositionCompletionsMsg{X: x, Y: y}
172 return completions.RepositionCompletionsMsg{X: x, Y: y}
173}
173}
174
174
175−func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175+func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
176 var cmd tea.Cmd
176 var cmd tea.Cmd
177 var cmds []tea.Cmd
177 var cmds []tea.Cmd
178 switch msg := msg.(type) {
178 switch msg := msg.(type) {
internal/tui/components/chat/header/header.go+1−1
@@ -44,7 +44,7 @@ func (h *header) Init() tea.Cmd {
44 return nil
44 return nil
45}
45}
46
46
47−func (h *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
47+func (h *header) Update(msg tea.Msg) (util.Model, tea.Cmd) {
48 switch msg := msg.(type) {
48 switch msg := msg.(type) {
49 case pubsub.Event[session.Session]:
49 case pubsub.Event[session.Session]:
50 if msg.Type == pubsub.UpdatedEvent {
50 if msg.Type == pubsub.UpdatedEvent {
internal/tui/components/chat/messages/messages.go+3−3
@@ -35,7 +35,7 @@ var ClearSelectionKey = key.NewBinding(key.WithKeys("esc", "alt+esc"), key.WithH
35// MessageCmp defines the interface for message components in the chat interface.
35// MessageCmp defines the interface for message components in the chat interface.
36// It combines standard UI model interfaces with message-specific functionality.
36// It combines standard UI model interfaces with message-specific functionality.
37type MessageCmp interface {
37type MessageCmp interface {
38− util.Model // Basic Bubble Tea model interface
38+ util.Model // Basic Bubble util.Model interface
39 layout.Sizeable // Width/height management
39 layout.Sizeable // Width/height management
40 layout.Focusable // Focus state management
40 layout.Focusable // Focus state management
41 GetMessage() message.Message // Access to underlying message data
41 GetMessage() message.Message // Access to underlying message data
@@ -94,7 +94,7 @@ func (m *messageCmp) Init() tea.Cmd {
94
94
95// Update handles incoming messages and updates the component state.
95// Update handles incoming messages and updates the component state.
96// Manages animation updates for spinning messages and stops animation when appropriate.
96// Manages animation updates for spinning messages and stops animation when appropriate.
97−func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
97+func (m *messageCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
98 switch msg := msg.(type) {
98 switch msg := msg.(type) {
99 case anim.StepMsg:
99 case anim.StepMsg:
100 m.spinning = m.shouldSpin()
100 m.spinning = m.shouldSpin()
@@ -384,7 +384,7 @@ func (m *assistantSectionModel) Init() tea.Cmd {
384 return nil
384 return nil
385}
385}
386
386
387−func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
387+func (m *assistantSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) {
388 return m, nil
388 return m, nil
389}
389}
390
390
internal/tui/components/chat/messages/tool.go+3−3
@@ -27,7 +27,7 @@ import (
27// ToolCallCmp defines the interface for tool call components in the chat interface.
27// ToolCallCmp defines the interface for tool call components in the chat interface.
28// It manages the display of tool execution including pending states, results, and errors.
28// It manages the display of tool execution including pending states, results, and errors.
29type ToolCallCmp interface {
29type ToolCallCmp interface {
30− util.Model // Basic Bubble Tea model interface
30+ util.Model // Basic Bubble util.Model interface
31 layout.Sizeable // Width/height management
31 layout.Sizeable // Width/height management
32 layout.Focusable // Focus state management
32 layout.Focusable // Focus state management
33 GetToolCall() message.ToolCall // Access to tool call data
33 GetToolCall() message.ToolCall // Access to tool call data
@@ -147,7 +147,7 @@ func (m *toolCallCmp) Init() tea.Cmd {
147
147
148// Update handles incoming messages and updates the component state.
148// Update handles incoming messages and updates the component state.
149// Manages animation updates for pending tool calls.
149// Manages animation updates for pending tool calls.
150−func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
150+func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
151 switch msg := msg.(type) {
151 switch msg := msg.(type) {
152 case anim.StepMsg:
152 case anim.StepMsg:
153 var cmds []tea.Cmd
153 var cmds []tea.Cmd
@@ -160,7 +160,7 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
160 }
160 }
161 if m.spinning {
161 if m.spinning {
162 u, cmd := m.anim.Update(msg)
162 u, cmd := m.anim.Update(msg)
163− m.anim = u.(util.Model)
163+ m.anim = u
164 cmds = append(cmds, cmd)
164 cmds = append(cmds, cmd)
165 }
165 }
166 return m, tea.Batch(cmds...)
166 return m, tea.Batch(cmds...)
internal/tui/components/chat/sidebar/sidebar.go+1−1
@@ -88,7 +88,7 @@ func (m *sidebarCmp) Init() tea.Cmd {
88 return nil
88 return nil
89}
89}
90
90
91−func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
91+func (m *sidebarCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
92 switch msg := msg.(type) {
92 switch msg := msg.(type) {
93 case SessionFilesMsg:
93 case SessionFilesMsg:
94 m.files = csync.NewMap[string, SessionFile]()
94 m.files = csync.NewMap[string, SessionFile]()
internal/tui/components/chat/splash/splash.go+1−1
@@ -135,7 +135,7 @@ func (s *splashCmp) SetSize(width int, height int) tea.Cmd {
135}
135}
136
136
137// Update implements SplashPage.
137// Update implements SplashPage.
138−func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
138+func (s *splashCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
139 switch msg := msg.(type) {
139 switch msg := msg.(type) {
140 case tea.WindowSizeMsg:
140 case tea.WindowSizeMsg:
141 return s, s.SetSize(msg.Width, msg.Height)
141 return s, s.SetSize(msg.Width, msg.Height)
internal/tui/components/completions/completions.go+1−1
@@ -112,7 +112,7 @@ func (c *completionsCmp) Init() tea.Cmd {
112}
112}
113
113
114// Update implements Completions.
114// Update implements Completions.
115−func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
115+func (c *completionsCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
116 switch msg := msg.(type) {
116 switch msg := msg.(type) {
117 case tea.WindowSizeMsg:
117 case tea.WindowSizeMsg:
118 c.wWidth, c.wHeight = msg.Width, msg.Height
118 c.wWidth, c.wHeight = msg.Width, msg.Height
internal/tui/components/core/status/status.go+1−1
@@ -36,7 +36,7 @@ func (m *statusCmp) Init() tea.Cmd {
36 return nil
36 return nil
37}
37}
38
38
39−func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
39+func (m *statusCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
40 switch msg := msg.(type) {
40 switch msg := msg.(type) {
41 case tea.WindowSizeMsg:
41 case tea.WindowSizeMsg:
42 m.width = msg.Width
42 m.width = msg.Width
internal/tui/components/dialogs/commands/arguments.go+1−1
@@ -92,7 +92,7 @@ func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
92}
92}
93
93
94// Update implements CommandArgumentsDialog.
94// Update implements CommandArgumentsDialog.
95−func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
95+func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
96 switch msg := msg.(type) {
96 switch msg := msg.(type) {
97 case tea.WindowSizeMsg:
97 case tea.WindowSizeMsg:
98 c.wWidth = msg.Width
98 c.wWidth = msg.Width
internal/tui/components/dialogs/commands/commands.go+1−1
@@ -116,7 +116,7 @@ func (c *commandDialogCmp) Init() tea.Cmd {
116 return c.SetCommandType(c.commandType)
116 return c.SetCommandType(c.commandType)
117}
117}
118
118
119−func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119+func (c *commandDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
120 switch msg := msg.(type) {
120 switch msg := msg.(type) {
121 case tea.WindowSizeMsg:
121 case tea.WindowSizeMsg:
122 c.wWidth = msg.Width
122 c.wWidth = msg.Width
internal/tui/components/dialogs/compact/compact.go+1−1
@@ -61,7 +61,7 @@ func (c *compactDialogCmp) Init() tea.Cmd {
61 return nil
61 return nil
62}
62}
63
63
64−func (c *compactDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
64+func (c *compactDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
65 switch msg := msg.(type) {
65 switch msg := msg.(type) {
66 case tea.WindowSizeMsg:
66 case tea.WindowSizeMsg:
67 c.wWidth = msg.Width
67 c.wWidth = msg.Width
internal/tui/components/dialogs/dialogs.go+7−3
@@ -32,7 +32,7 @@ type CloseDialogMsg struct{}
32
32
33// DialogCmp manages a stack of dialogs with keyboard navigation.
33// DialogCmp manages a stack of dialogs with keyboard navigation.
34type DialogCmp interface {
34type DialogCmp interface {
35− tea.Model
35+ util.Model
36
36
37 Dialogs() []DialogModel
37 Dialogs() []DialogModel
38 HasDialogs() bool
38 HasDialogs() bool
@@ -62,7 +62,7 @@ func (d dialogCmp) Init() tea.Cmd {
62}
62}
63
63
64// Update handles dialog lifecycle and forwards messages to the active dialog.
64// Update handles dialog lifecycle and forwards messages to the active dialog.
65−func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
65+func (d dialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
66 switch msg := msg.(type) {
66 switch msg := msg.(type) {
67 case tea.WindowSizeMsg:
67 case tea.WindowSizeMsg:
68 var cmds []tea.Cmd
68 var cmds []tea.Cmd
@@ -98,7 +98,11 @@ func (d dialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
98 return d, nil
98 return d, nil
99}
99}
100
100
101+func (d dialogCmp) View() string {
102+ return ""
103+}
104+
101−func (d dialogCmp) handleOpen(msg OpenDialogMsg) (tea.Model, tea.Cmd) {
105+func (d dialogCmp) handleOpen(msg OpenDialogMsg) (util.Model, tea.Cmd) {
102 if d.HasDialogs() {
106 if d.HasDialogs() {
103 dialog := d.dialogs[len(d.dialogs)-1]
107 dialog := d.dialogs[len(d.dialogs)-1]
104 if dialog.ID() == msg.Model.ID() {
108 if dialog.ID() == msg.Model.ID() {
internal/tui/components/dialogs/filepicker/filepicker.go+1−1
@@ -88,7 +88,7 @@ func (m *model) Init() tea.Cmd {
88 return m.filePicker.Init()
88 return m.filePicker.Init()
89}
89}
90
90
91−func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
91+func (m *model) Update(msg tea.Msg) (util.Model, tea.Cmd) {
92 switch msg := msg.(type) {
92 switch msg := msg.(type) {
93 case tea.WindowSizeMsg:
93 case tea.WindowSizeMsg:
94 m.wWidth = msg.Width
94 m.wWidth = msg.Width
internal/tui/components/dialogs/models/apikey.go+2−1
@@ -9,6 +9,7 @@ import (
9 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/home"
10 "github.com/charmbracelet/crush/internal/home"
11 "github.com/charmbracelet/crush/internal/tui/styles"
11 "github.com/charmbracelet/crush/internal/tui/styles"
12+ "github.com/charmbracelet/crush/internal/tui/util"
12 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/lipgloss/v2"
13)
14)
14
15
@@ -75,7 +76,7 @@ func (a *APIKeyInput) Init() tea.Cmd {
75 return a.spinner.Tick
76 return a.spinner.Tick
76}
77}
77
78
78−func (a *APIKeyInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
79+func (a *APIKeyInput) Update(msg tea.Msg) (util.Model, tea.Cmd) {
79 switch msg := msg.(type) {
80 switch msg := msg.(type) {
80 case spinner.TickMsg:
81 case spinner.TickMsg:
81 if a.state == APIKeyInputStateVerifying {
82 if a.state == APIKeyInputStateVerifying {
internal/tui/components/dialogs/models/models.go+1−1
@@ -98,7 +98,7 @@ func (m *modelDialogCmp) Init() tea.Cmd {
98 return tea.Batch(m.modelList.Init(), m.apiKeyInput.Init())
98 return tea.Batch(m.modelList.Init(), m.apiKeyInput.Init())
99}
99}
100
100
101−func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101+func (m *modelDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
102 switch msg := msg.(type) {
102 switch msg := msg.(type) {
103 case tea.WindowSizeMsg:
103 case tea.WindowSizeMsg:
104 m.wWidth = msg.Width
104 m.wWidth = msg.Width
internal/tui/components/dialogs/permissions/permissions.go+1−1
@@ -95,7 +95,7 @@ func (p *permissionDialogCmp) supportsDiffView() bool {
95 return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName
95 return p.permission.ToolName == tools.EditToolName || p.permission.ToolName == tools.WriteToolName || p.permission.ToolName == tools.MultiEditToolName
96}
96}
97
97
98−func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
98+func (p *permissionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
99 var cmds []tea.Cmd
99 var cmds []tea.Cmd
100
100
101 switch msg := msg.(type) {
101 switch msg := msg.(type) {
internal/tui/components/dialogs/quit/quit.go+1−1
@@ -40,7 +40,7 @@ func (q *quitDialogCmp) Init() tea.Cmd {
40}
40}
41
41
42// Update handles keyboard input for the quit dialog.
42// Update handles keyboard input for the quit dialog.
43−func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
43+func (q *quitDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
44 switch msg := msg.(type) {
44 switch msg := msg.(type) {
45 case tea.WindowSizeMsg:
45 case tea.WindowSizeMsg:
46 q.wWidth = msg.Width
46 q.wWidth = msg.Width
internal/tui/components/dialogs/reasoning/reasoning.go+1−1
@@ -172,7 +172,7 @@ func (r *reasoningDialogCmp) populateEffortOptions() tea.Cmd {
172 return nil
172 return nil
173}
173}
174
174
175−func (r *reasoningDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
175+func (r *reasoningDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
176 switch msg := msg.(type) {
176 switch msg := msg.(type) {
177 case tea.WindowSizeMsg:
177 case tea.WindowSizeMsg:
178 r.wWidth = msg.Width
178 r.wWidth = msg.Width
internal/tui/components/dialogs/sessions/sessions.go+1−1
@@ -81,7 +81,7 @@ func (s *sessionDialogCmp) Init() tea.Cmd {
81 return tea.Sequence(cmds...)
81 return tea.Sequence(cmds...)
82}
82}
83
83
84−func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
84+func (s *sessionDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
85 switch msg := msg.(type) {
85 switch msg := msg.(type) {
86 case tea.WindowSizeMsg:
86 case tea.WindowSizeMsg:
87 var cmds []tea.Cmd
87 var cmds []tea.Cmd
internal/tui/exp/list/filterable.go+2−1
@@ -9,6 +9,7 @@ import (
9 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
10 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
11 "github.com/charmbracelet/crush/internal/tui/styles"
11 "github.com/charmbracelet/crush/internal/tui/styles"
12+ "github.com/charmbracelet/crush/internal/tui/util"
12 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/sahilm/fuzzy"
14 "github.com/sahilm/fuzzy"
14)
15)
@@ -116,7 +117,7 @@ func NewFilterableList[T FilterableItem](items []T, opts ...filterableListOption
116 return f
117 return f
117}
118}
118
119
119−func (f *filterableList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
120+func (f *filterableList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) {
120 switch msg := msg.(type) {
121 switch msg := msg.(type) {
121 case tea.KeyPressMsg:
122 case tea.KeyPressMsg:
122 switch {
123 switch {
internal/tui/exp/list/filterable_group.go+2−1
@@ -11,6 +11,7 @@ import (
11 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
12 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
12 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
13 "github.com/charmbracelet/crush/internal/tui/styles"
13 "github.com/charmbracelet/crush/internal/tui/styles"
14+ "github.com/charmbracelet/crush/internal/tui/util"
14 "github.com/charmbracelet/lipgloss/v2"
15 "github.com/charmbracelet/lipgloss/v2"
15 "github.com/sahilm/fuzzy"
16 "github.com/sahilm/fuzzy"
16)
17)
@@ -65,7 +66,7 @@ func NewFilterableGroupedList[T FilterableItem](items []Group[T], opts ...filter
65 return f
66 return f
66}
67}
67
68
68−func (f *filterableGroupList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
69+func (f *filterableGroupList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) {
69 switch msg := msg.(type) {
70 switch msg := msg.(type) {
70 case tea.KeyPressMsg:
71 case tea.KeyPressMsg:
71 switch {
72 switch {
internal/tui/exp/list/grouped.go+1−1
@@ -58,7 +58,7 @@ func (g *groupedList[T]) Init() tea.Cmd {
58 return g.render()
58 return g.render()
59}
59}
60
60
61−func (l *groupedList[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
61+func (l *groupedList[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) {
62 u, cmd := l.list.Update(msg)
62 u, cmd := l.list.Update(msg)
63 l.list = u.(*list[Item])
63 l.list = u.(*list[Item])
64 return l, cmd
64 return l, cmd
internal/tui/exp/list/items.go+3−2
@@ -7,6 +7,7 @@ import (
7 "github.com/charmbracelet/crush/internal/tui/components/core"
7 "github.com/charmbracelet/crush/internal/tui/components/core"
8 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
9 "github.com/charmbracelet/crush/internal/tui/styles"
9 "github.com/charmbracelet/crush/internal/tui/styles"
10+ "github.com/charmbracelet/crush/internal/tui/util"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/ansi"
12 "github.com/charmbracelet/x/ansi"
12 "github.com/google/uuid"
13 "github.com/google/uuid"
@@ -97,7 +98,7 @@ func (c *completionItemCmp[T]) Init() tea.Cmd {
97}
98}
98
99
99// Update implements CommandItem.
100// Update implements CommandItem.
100−func (c *completionItemCmp[T]) Update(tea.Msg) (tea.Model, tea.Cmd) {
101+func (c *completionItemCmp[T]) Update(tea.Msg) (util.Model, tea.Cmd) {
101 return c, nil
102 return c, nil
102}
103}
103
104
@@ -348,7 +349,7 @@ func (m *itemSectionModel) Init() tea.Cmd {
348 return nil
349 return nil
349}
350}
350
351
351−func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
352+func (m *itemSectionModel) Update(tea.Msg) (util.Model, tea.Cmd) {
352 return m, nil
353 return m, nil
353}
354}
354
355
internal/tui/exp/list/list.go+2−2
@@ -217,7 +217,7 @@ func (l *list[T]) Init() tea.Cmd {
217}
217}
218
218
219// Update implements List.
219// Update implements List.
220−func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
220+func (l *list[T]) Update(msg tea.Msg) (util.Model, tea.Cmd) {
221 switch msg := msg.(type) {
221 switch msg := msg.(type) {
222 case tea.MouseWheelMsg:
222 case tea.MouseWheelMsg:
223 if l.enableMouse {
223 if l.enableMouse {
@@ -277,7 +277,7 @@ func (l *list[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
277 return l, nil
277 return l, nil
278}
278}
279
279
280−func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (tea.Model, tea.Cmd) {
280+func (l *list[T]) handleMouseWheel(msg tea.MouseWheelMsg) (util.Model, tea.Cmd) {
281 var cmd tea.Cmd
281 var cmd tea.Cmd
282 switch msg.Button {
282 switch msg.Button {
283 case tea.MouseWheelDown:
283 case tea.MouseWheelDown:
internal/tui/exp/list/list_test.go+3−2
@@ -7,6 +7,7 @@ import (
7
7
8 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
9 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
10+ "github.com/charmbracelet/crush/internal/tui/util"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/x/exp/golden"
12 "github.com/charmbracelet/x/exp/golden"
12 "github.com/google/uuid"
13 "github.com/google/uuid"
@@ -602,7 +603,7 @@ func (s *simpleItem) Init() tea.Cmd {
602 return nil
603 return nil
603}
604}
604
605
605−func (s *simpleItem) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
606+func (s *simpleItem) Update(msg tea.Msg) (util.Model, tea.Cmd) {
606 return s, nil
607 return s, nil
607}
608}
608
609
@@ -644,7 +645,7 @@ func (s *selectableItem) IsFocused() bool {
644 return s.focused
645 return s.focused
645}
646}
646
647
647−func execCmd(m tea.Model, cmd tea.Cmd) {
648+func execCmd(m util.Model, cmd tea.Cmd) {
648 for cmd != nil {
649 for cmd != nil {
649 msg := cmd()
650 msg := cmd()
650 m, cmd = m.Update(msg)
651 m, cmd = m.Update(msg)
internal/tui/page/chat/chat.go+1−1
@@ -163,7 +163,7 @@ func (p *chatPage) Init() tea.Cmd {
163 )
163 )
164}
164}
165
165
166−func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
166+func (p *chatPage) Update(msg tea.Msg) (util.Model, tea.Cmd) {
167 var cmds []tea.Cmd
167 var cmds []tea.Cmd
168 switch msg := msg.(type) {
168 switch msg := msg.(type) {
169 case tea.KeyboardEnhancementsMsg:
169 case tea.KeyboardEnhancementsMsg:
internal/tui/tui.go+10−26
@@ -91,8 +91,6 @@ func (a appModel) Init() tea.Cmd {
91 cmd = a.status.Init()
91 cmd = a.status.Init()
92 cmds = append(cmds, cmd)
92 cmds = append(cmds, cmd)
93
93
94− cmds = append(cmds, tea.EnableMouseAllMotion)
95−
96 return tea.Batch(cmds...)
94 return tea.Batch(cmds...)
97}
95}
98
96
@@ -106,9 +104,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
106 case tea.KeyboardEnhancementsMsg:
104 case tea.KeyboardEnhancementsMsg:
107 for id, page := range a.pages {
105 for id, page := range a.pages {
108 m, pageCmd := page.Update(msg)
106 m, pageCmd := page.Update(msg)
109− if model, ok := m.(util.Model); ok {
110− a.pages[id] = model
107+ a.pages[id] = m
111− }
112
108
113 if pageCmd != nil {
109 if pageCmd != nil {
114 cmds = append(cmds, pageCmd)
110 cmds = append(cmds, pageCmd)
@@ -232,9 +228,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
232
228
233 // Forward to view.
229 // Forward to view.
234 updated, itemCmd := item.Update(msg)
230 updated, itemCmd := item.Update(msg)
235− if model, ok := updated.(util.Model); ok {
236− a.pages[a.currentPage] = model
231+ a.pages[a.currentPage] = updated
237− }
238
232
239 return a, itemCmd
233 return a, itemCmd
240 case pubsub.Event[permission.PermissionRequest]:
234 case pubsub.Event[permission.PermissionRequest]:
@@ -292,9 +286,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
292
286
293 a.isConfigured = config.HasInitialDataConfig()
287 a.isConfigured = config.HasInitialDataConfig()
294 updated, pageCmd := item.Update(msg)
288 updated, pageCmd := item.Update(msg)
295− if model, ok := updated.(util.Model); ok {
296− a.pages[a.currentPage] = model
289+ a.pages[a.currentPage] = updated
297− }
298
290
299 cmds = append(cmds, pageCmd)
291 cmds = append(cmds, pageCmd)
300 return a, tea.Batch(cmds...)
292 return a, tea.Batch(cmds...)
@@ -314,9 +306,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
314 }
306 }
315
307
316 updated, pageCmd := item.Update(msg)
308 updated, pageCmd := item.Update(msg)
317− if model, ok := updated.(util.Model); ok {
318− a.pages[a.currentPage] = model
309+ a.pages[a.currentPage] = updated
319− }
320
310
321 cmds = append(cmds, pageCmd)
311 cmds = append(cmds, pageCmd)
322 }
312 }
@@ -336,9 +326,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
336 }
326 }
337
327
338 updated, pageCmd := item.Update(msg)
328 updated, pageCmd := item.Update(msg)
339− if model, ok := updated.(util.Model); ok {
340− a.pages[a.currentPage] = model
329+ a.pages[a.currentPage] = updated
341− }
342
330
343 cmds = append(cmds, pageCmd)
331 cmds = append(cmds, pageCmd)
344 }
332 }
@@ -353,9 +341,7 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
353 }
341 }
354
342
355 updated, cmd := item.Update(msg)
343 updated, cmd := item.Update(msg)
356− if model, ok := updated.(util.Model); ok {
357− a.pages[a.currentPage] = model
344+ a.pages[a.currentPage] = updated
358− }
359
345
360 if a.dialog.HasDialogs() {
346 if a.dialog.HasDialogs() {
361 u, dialogCmd := a.dialog.Update(msg)
347 u, dialogCmd := a.dialog.Update(msg)
@@ -391,9 +377,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd {
391 // Update the current view.
377 // Update the current view.
392 for p, page := range a.pages {
378 for p, page := range a.pages {
393 updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height})
379 updated, pageCmd := page.Update(tea.WindowSizeMsg{Width: width, Height: height})
394− if model, ok := updated.(util.Model); ok {
395− a.pages[p] = model
380+ a.pages[p] = updated
396− }
397
381
398 cmds = append(cmds, pageCmd)
382 cmds = append(cmds, pageCmd)
399 }
383 }
@@ -496,9 +480,7 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
496 }
480 }
497
481
498 updated, cmd := item.Update(msg)
482 updated, cmd := item.Update(msg)
499− if model, ok := updated.(util.Model); ok {
500− a.pages[a.currentPage] = model
483+ a.pages[a.currentPage] = updated
501− }
502 return cmd
484 return cmd
503 }
485 }
504}
486}
@@ -602,6 +584,8 @@ func (a *appModel) View() tea.View {
602
584
603 view.Layer = canvas
585 view.Layer = canvas
604 view.Cursor = cursor
586 view.Cursor = cursor
587+ view.MouseMode = tea.MouseModeCellMotion
588+ view.AltScreen = true
605 if a.app != nil && a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
589 if a.app != nil && a.app.CoderAgent != nil && a.app.CoderAgent.IsBusy() {
606 // HACK: use a random percentage to prevent ghostty from hiding it
590 // HACK: use a random percentage to prevent ghostty from hiding it
607 // after a timeout.
591 // after a timeout.
internal/tui/util/util.go+3−2
@@ -12,8 +12,9 @@ type Cursor interface {
12}
12}
13
13
14type Model interface {
14type Model interface {
15− tea.Model
15+ Init() tea.Cmd
16− tea.ViewModel
16+ Update(tea.Msg) (Model, tea.Cmd)
17+ View() string
17}
18}
18
19
19func CmdHandler(msg tea.Msg) tea.Cmd {
20func CmdHandler(msg tea.Msg) tea.Cmd {