on main help sign in
refactor: use uv bubbletea
commit187e91508a5abcb716abd82f93583dc8bce74155 authorAyman Bagabas <ayman.bagabas@gmail.com> dateMon 23 Jun 2025, 14:50 (1 year ago) parent431a6c4
33 files changed+215−173
go.mod+14−4 go.sum+4−8 internal/tui/components/anim/anim.go+2−2 internal/tui/components/chat/chat.go+4−6 internal/tui/components/chat/editor/editor.go+8−8 internal/tui/components/chat/header/header.go+3−3 internal/tui/components/chat/messages/messages.go+8−10 internal/tui/components/chat/messages/renderer.go+1−1 internal/tui/components/chat/messages/tool.go+4−4 internal/tui/components/chat/sidebar/sidebar.go+2−4 internal/tui/components/completions/completions.go+3−6 internal/tui/components/completions/item.go+2−2 internal/tui/components/core/layout/container.go+9−5 internal/tui/components/core/layout/split.go+24−19 internal/tui/components/core/list/list.go+13−9 internal/tui/components/core/status/status.go+2−2 internal/tui/components/dialogs/commands/arguments.go+10−10 internal/tui/components/dialogs/commands/commands.go+14−8 internal/tui/components/dialogs/commands/item.go+2−2 internal/tui/components/dialogs/compact/compact.go+2−2 internal/tui/components/dialogs/dialogs.go+4−5 internal/tui/components/dialogs/filepicker/filepicker.go+2−2 internal/tui/components/dialogs/init/init.go+2−2 internal/tui/components/dialogs/models/models.go+13−7 internal/tui/components/dialogs/permissions/permissions.go+2−2 internal/tui/components/dialogs/quit/quit.go+2−4 internal/tui/components/dialogs/sessions/sessions.go+13−7 internal/tui/components/logs/details.go+2−2 internal/tui/components/logs/table.go+2−2 internal/tui/page/chat/chat.go+12−7 internal/tui/page/logs/logs.go+6−8 internal/tui/tui.go+19−9 internal/tui/util/util.go+5−1
go.mod+14−4
@@ -1,6 +1,12 @@
11module github.com/charmbracelet/crush
22
3−go 1.24.0
3+go 1.24.3
4+
5+replace github.com/charmbracelet/bubbletea/v2 => ../bubbletea
6+
7+replace github.com/charmbracelet/lipgloss/v2 => ../lipgloss
8+
9+replace github.com/charmbracelet/uv => ../uv
410
511require (
612 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
@@ -17,7 +23,7 @@ require (
1723 github.com/charmbracelet/fang v0.1.0
1824 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
1925 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
20− github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
26+ github.com/charmbracelet/x/ansi v0.9.3
2127 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444
2228 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
2329 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -41,7 +47,11 @@ require (
4147)
4248
4349require (
44− golang.org/x/term v0.31.0 // indirect
50+ github.com/charmbracelet/uv v0.0.0-00010101000000-000000000000 // indirect
51+ github.com/charmbracelet/x/termios v0.1.1 // indirect
52+)
53+
54+require (
4555 cloud.google.com/go v0.116.0 // indirect
4656 cloud.google.com/go/auth v0.13.0 // indirect
4757 cloud.google.com/go/compute/metadata v0.6.0 // indirect
@@ -69,7 +79,6 @@ require (
6979 github.com/charmbracelet/colorprofile v0.3.1 // indirect
7080 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
7181 github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
72− github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
7382 github.com/charmbracelet/x/term v0.2.1 // indirect
7483 github.com/charmbracelet/x/windows v0.2.1 // indirect
7584 github.com/davecgh/go-spew v1.1.1 // indirect
@@ -132,6 +141,7 @@ require (
132141 golang.org/x/net v0.39.0 // indirect
133142 golang.org/x/sync v0.13.0 // indirect
134143 golang.org/x/sys v0.32.0 // indirect
144+ golang.org/x/term v0.31.0 // indirect
135145 golang.org/x/text v0.24.0 // indirect
136146 google.golang.org/genai v1.3.0
137147 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
go.sum+4−8
@@ -70,18 +70,14 @@ github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr
7070github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
7171github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
7272github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
73−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94 h1:QIi50k+uNTJmp2sMs+33D1m/EWr/7OPTJ8x92AY3eOc=
74−github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3.0.20250609143341-c76fa36f1b94/go.mod h1:oOn1YZGZyJHxJfh4sFAna9vDzxJRNuErLETr/lnlB/I=
7573github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
7674github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
7775github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg=
7876github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
7977github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
8078github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
81−github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c h1:177KMz8zHRlEZJsWzafbKYh6OdjgvTspoH+UjaxgIXY=
82−github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ=
83−github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413 h1:L07QkDqRF274IZ2UJ/mCTL8DR95efU9BNWLYCDXEjvQ=
84−github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
79+github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
80+github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
8581github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
8682github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
8783github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
@@ -90,10 +86,10 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHE
9086github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
9187github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef h1:v7qwsZ2OxzlwvpKwz8dtZXp7fIJlcDEUOyFBNE4fz4Q=
9288github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
93−github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
94−github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
9589github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
9690github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
91+github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
92+github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
9793github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
9894github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
9995github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
internal/tui/components/anim/anim.go+2−2
@@ -257,7 +257,7 @@ func (a *anim) updateChars(chars *[]cyclingChar) {
257257}
258258
259259// View renders the animation.
260−func (a anim) View() tea.View {
260+func (a anim) View() string {
261261 var (
262262 t = styles.CurrentTheme()
263263 b strings.Builder
@@ -289,7 +289,7 @@ func (a anim) View() tea.View {
289289 b.WriteString(textStyle.Render(a.ellipsis.View()))
290290 }
291291
292− return tea.NewView(b.String())
292+ return b.String()
293293}
294294
295295func GetColor(c color.Color) string {
internal/tui/components/chat/chat.go+4−6
@@ -101,12 +101,10 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
101101}
102102
103103// View renders the message list or an initial screen if empty.
104−func (m *messageListCmp) View() tea.View {
105− return tea.NewView(
106− lipgloss.JoinVertical(
107− lipgloss.Left,
108− m.listCmp.View().String(),
109− ),
104+func (m *messageListCmp) View() string {
105+ return lipgloss.JoinVertical(
106+ lipgloss.Left,
107+ m.listCmp.View(),
110108 )
111109}
112110
internal/tui/components/chat/editor/editor.go+8−8
@@ -265,20 +265,22 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
265265 return m, tea.Batch(cmds...)
266266}
267267
268−func (m *editorCmp) View() tea.View {
269− t := styles.CurrentTheme()
268+func (m *editorCmp) Cursor() *tea.Cursor {
270269 cursor := m.textarea.Cursor()
271270 if cursor != nil {
272271 cursor.X = cursor.X + m.x + 1
273272 cursor.Y = cursor.Y + m.y + 1 // adjust for padding
274273 }
274+ return cursor
275+}
276+
277+func (m *editorCmp) View() string {
278+ t := styles.CurrentTheme()
275279 if len(m.attachments) == 0 {
276280 content := t.S().Base.Padding(1).Render(
277281 m.textarea.View(),
278282 )
279− view := tea.NewView(content)
280− view.SetCursor(cursor)
281− return view
283+ return content
282284 }
283285 content := t.S().Base.Padding(0, 1, 1, 1).Render(
284286 lipgloss.JoinVertical(lipgloss.Top,
@@ -286,9 +288,7 @@ func (m *editorCmp) View() tea.View {
286288 m.textarea.View(),
287289 ),
288290 )
289− view := tea.NewView(content)
290− view.SetCursor(cursor)
291− return view
291+ return content
292292}
293293
294294func (m *editorCmp) SetSize(width, height int) tea.Cmd {
internal/tui/components/chat/header/header.go+3−3
@@ -58,9 +58,9 @@ func (p *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5858 return p, nil
5959}
6060
61−func (p *header) View() tea.View {
61+func (p *header) View() string {
6262 if p.session.ID == "" {
63− return tea.NewView("")
63+ return ""
6464 }
6565
6666 t := styles.CurrentTheme()
@@ -87,7 +87,7 @@ func (p *header) View() tea.View {
8787 parts...,
8888 ),
8989 )
90− return tea.NewView(content)
90+ return content
9191}
9292
9393func (h *header) details() string {
internal/tui/components/chat/messages/messages.go+8−10
@@ -83,20 +83,20 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8383
8484// View renders the message component based on its current state.
8585// Returns different views for spinning, user, and assistant messages.
86−func (m *messageCmp) View() tea.View {
86+func (m *messageCmp) View() string {
8787 if m.spinning {
88− return tea.NewView(m.style().PaddingLeft(1).Render(m.anim.View().String()))
88+ return m.style().PaddingLeft(1).Render(m.anim.View())
8989 }
9090 if m.message.ID != "" {
9191 // this is a user or assistant message
9292 switch m.message.Role {
9393 case message.User:
94− return tea.NewView(m.renderUserMessage())
94+ return m.renderUserMessage()
9595 default:
96− return tea.NewView(m.renderAssistantMessage())
96+ return m.renderAssistantMessage()
9797 }
9898 }
99− return tea.NewView(m.style().Render("No message content"))
99+ return m.style().Render("No message content")
100100}
101101
102102// GetMessage returns the underlying message data
@@ -283,7 +283,7 @@ func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
283283 return m, nil
284284}
285285
286−func (m *assistantSectionModel) View() tea.View {
286+func (m *assistantSectionModel) View() string {
287287 t := styles.CurrentTheme()
288288 finishData := m.message.FinishPart()
289289 finishTime := time.Unix(finishData.Time, 0)
@@ -292,10 +292,8 @@ func (m *assistantSectionModel) View() tea.View {
292292 icon := t.S().Subtle.Render(styles.ModelIcon)
293293 model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
294294 assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
295− return tea.NewView(
296− t.S().Base.PaddingLeft(2).Render(
297− core.Section(assistant, m.width-2),
298− ),
295+ return t.S().Base.PaddingLeft(2).Render(
296+ core.Section(assistant, m.width-2),
299297 )
300298}
301299
internal/tui/components/chat/messages/renderer.go+1−1
@@ -542,7 +542,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
542542
543543 if v.result.ToolCallID == "" {
544544 v.spinning = true
545− parts = append(parts, v.anim.View().String())
545+ parts = append(parts, v.anim.View())
546546 } else {
547547 v.spinning = false
548548 }
internal/tui/components/chat/messages/tool.go+4−4
@@ -133,19 +133,19 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
133133
134134// View renders the tool call component based on its current state.
135135// Shows either a pending animation or the tool-specific rendered result.
136−func (m *toolCallCmp) View() tea.View {
136+func (m *toolCallCmp) View() string {
137137 box := m.style()
138138
139139 if !m.call.Finished && !m.cancelled {
140− return tea.NewView(box.Render(m.renderPending()))
140+ return box.Render(m.renderPending())
141141 }
142142
143143 r := registry.lookup(m.call.Name)
144144
145145 if m.isNested {
146− return tea.NewView(box.Render(r.Render(m)))
146+ return box.Render(r.Render(m))
147147 }
148− return tea.NewView(box.Render(r.Render(m)))
148+ return box.Render(r.Render(m))
149149}
150150
151151// State management methods
internal/tui/components/chat/sidebar/sidebar.go+2−4
@@ -107,7 +107,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107107 return m, nil
108108}
109109
110−func (m *sidebarCmp) View() tea.View {
110+func (m *sidebarCmp) View() string {
111111 t := styles.CurrentTheme()
112112 parts := []string{}
113113 if !m.compactMode {
@@ -139,9 +139,7 @@ func (m *sidebarCmp) View() tea.View {
139139 m.mcpBlock(),
140140 )
141141
142− return tea.NewView(
143− lipgloss.JoinVertical(lipgloss.Left, parts...),
144− )
142+ return lipgloss.JoinVertical(lipgloss.Left, parts...)
145143}
146144
147145func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
internal/tui/components/completions/completions.go+3−6
@@ -157,15 +157,12 @@ func (c *completionsCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
157157}
158158
159159// View implements Completions.
160−func (c *completionsCmp) View() tea.View {
160+func (c *completionsCmp) View() string {
161161 if len(c.list.Items()) == 0 {
162− return tea.NewView(c.style().Render("No completions found"))
162+ return c.style().Render("No completions found")
163163 }
164164
165− view := tea.NewView(
166− c.style().Render(c.list.View().String()),
167− )
168− return view
165+ return c.style().Render(c.list.View())
169166}
170167
171168func (c *completionsCmp) style() lipgloss.Style {
internal/tui/components/completions/item.go+2−2
@@ -75,7 +75,7 @@ func (c *completionItemCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
7575}
7676
7777// View implements CommandItem.
78−func (c *completionItemCmp) View() tea.View {
78+func (c *completionItemCmp) View() string {
7979 t := styles.CurrentTheme()
8080
8181 itemStyle := t.S().Base.Padding(0, 1).Width(c.width)
@@ -135,7 +135,7 @@ func (c *completionItemCmp) View() tea.View {
135135 parts...,
136136 ),
137137 )
138− return tea.NewView(item)
138+ return item
139139}
140140
141141// Blur implements CommandItem.
internal/tui/components/core/layout/container.go+9−5
@@ -72,7 +72,14 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7272 }
7373}
7474
75−func (c *container) View() tea.View {
75+func (c *container) Cursor() *tea.Cursor {
76+ if cursor, ok := c.content.(util.Cursor); ok {
77+ return cursor.Cursor()
78+ }
79+ return nil
80+}
81+
82+func (c *container) View() string {
7683 t := styles.CurrentTheme()
7784 width := c.width
7885 height := c.height
@@ -106,10 +113,7 @@ func (c *container) View() tea.View {
106113 PaddingLeft(c.paddingLeft)
107114
108115 contentView := c.content.View()
109− view := tea.NewView(style.Render(contentView.String()))
110− cursor := contentView.Cursor()
111− view.SetCursor(cursor)
112− return view
116+ return style.Render(contentView)
113117}
114118
115119func (c *container) SetSize(width, height int) tea.Cmd {
internal/tui/components/core/layout/split.go+24−19
@@ -104,17 +104,34 @@ func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
104104 return s, tea.Batch(cmds...)
105105}
106106
107−func (s *splitPaneLayout) View() tea.View {
107+func (s *splitPaneLayout) Cursor() *tea.Cursor {
108+ if s.bottomPanel != nil {
109+ if c, ok := s.bottomPanel.(util.Cursor); ok {
110+ return c.Cursor()
111+ }
112+ } else if s.rightPanel != nil {
113+ if c, ok := s.rightPanel.(util.Cursor); ok {
114+ return c.Cursor()
115+ }
116+ } else if s.leftPanel != nil {
117+ if c, ok := s.leftPanel.(util.Cursor); ok {
118+ return c.Cursor()
119+ }
120+ }
121+ return nil
122+}
123+
124+func (s *splitPaneLayout) View() string {
108125 var topSection string
109126
110127 if s.leftPanel != nil && s.rightPanel != nil {
111128 leftView := s.leftPanel.View()
112129 rightView := s.rightPanel.View()
113− topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView.String(), rightView.String())
130+ topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
114131 } else if s.leftPanel != nil {
115− topSection = s.leftPanel.View().String()
132+ topSection = s.leftPanel.View()
116133 } else if s.rightPanel != nil {
117− topSection = s.rightPanel.View().String()
134+ topSection = s.rightPanel.View()
118135 } else {
119136 topSection = ""
120137 }
@@ -123,32 +140,20 @@ func (s *splitPaneLayout) View() tea.View {
123140
124141 if s.bottomPanel != nil && topSection != "" {
125142 bottomView := s.bottomPanel.View()
126− finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView.String())
143+ finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
127144 } else if s.bottomPanel != nil {
128− finalView = s.bottomPanel.View().String()
145+ finalView = s.bottomPanel.View()
129146 } else {
130147 finalView = topSection
131148 }
132149
133− // TODO: think of a better way to handle multiple cursors
134− var cursor *tea.Cursor
135− if s.bottomPanel != nil {
136− cursor = s.bottomPanel.View().Cursor()
137− } else if s.rightPanel != nil {
138− cursor = s.rightPanel.View().Cursor()
139− } else if s.leftPanel != nil {
140− cursor = s.leftPanel.View().Cursor()
141− }
142−
143150 t := styles.CurrentTheme()
144151
145152 style := t.S().Base.
146153 Width(s.width).
147154 Height(s.height)
148155
149− view := tea.NewView(style.Render(finalView))
150− view.SetCursor(cursor)
151− return view
156+ return style.Render(finalView)
152157}
153158
154159func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
internal/tui/components/core/list/list.go+13−9
@@ -281,12 +281,20 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
281281 return m, nil
282282}
283283
284+// Cursor returns the current cursor position in the input field.
285+func (m *model) Cursor() *tea.Cursor {
286+ if m.filterable && !m.hideFilterInput {
287+ return m.input.Cursor()
288+ }
289+ return nil
290+}
291+
284292// View renders the list to a string for display.
285293// Returns empty string if the list has no dimensions.
286294// Triggers re-rendering if needed before returning content.
287−func (m *model) View() tea.View {
295+func (m *model) View() string {
288296 if m.viewState.height == 0 || m.viewState.width == 0 {
289− return tea.NewView("") // No content to display
297+ return "" // No content to display
290298 }
291299 if m.renderState.needsRerender {
292300 m.renderVisible()
@@ -304,11 +312,7 @@ func (m *model) View() tea.View {
304312 content,
305313 )
306314 }
307− view := tea.NewView(content)
308− if m.filterable && !m.hideFilterInput {
309− view.SetCursor(m.input.Cursor())
310− }
311− return view
315+ return content
312316}
313317
314318// handleKeyPress processes keyboard input for list navigation.
@@ -834,7 +838,7 @@ func (m *model) rerenderItem(inx int) {
834838func (m *model) getItemLines(item util.Model) []string {
835839 var itemLines []string
836840
837− itemLines = strings.Split(item.View().String(), "\n")
841+ itemLines = strings.Split(item.View(), "\n")
838842
839843 if m.gapSize > 0 {
840844 gap := make([]string, m.gapSize)
@@ -1262,7 +1266,7 @@ func (m *model) filterSection(sect section, search string) *section {
12621266
12631267 // Check if section header itself matches
12641268 if sect.header != nil {
1265− headerText := strings.ToLower(sect.header.View().String())
1269+ headerText := strings.ToLower(sect.header.View())
12661270 if strings.Contains(headerText, search) {
12671271 hasHeaderMatch = true
12681272 // If header matches, include all items in the section
internal/tui/components/core/status/status.go+2−2
@@ -94,13 +94,13 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9494 return m, nil
9595}
9696
97−func (m *statusCmp) View() tea.View {
97+func (m *statusCmp) View() string {
9898 t := styles.CurrentTheme()
9999 status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
100100 if m.info.Msg != "" {
101101 status = m.infoMsg()
102102 }
103− return tea.NewView(status)
103+ return status
104104}
105105
106106func (m *statusCmp) infoMsg() string {
internal/tui/components/dialogs/commands/arguments.go+10−10
@@ -139,7 +139,7 @@ func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
139139}
140140
141141// View implements CommandArgumentsDialog.
142−func (c *commandArgumentsDialogCmp) View() tea.View {
142+func (c *commandArgumentsDialogCmp) View() string {
143143 t := styles.CurrentTheme()
144144 baseStyle := t.S().Base
145145
@@ -188,19 +188,19 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
188188 elements...,
189189 )
190190
191− view := tea.NewView(
192− baseStyle.Padding(1, 1, 0, 1).
193− Border(lipgloss.RoundedBorder()).
194− BorderForeground(t.BorderFocus).
195− Width(c.width).
196− Render(content),
197− )
191+ return baseStyle.Padding(1, 1, 0, 1).
192+ Border(lipgloss.RoundedBorder()).
193+ BorderForeground(t.BorderFocus).
194+ Width(c.width).
195+ Render(content)
196+}
197+
198+func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
198199 cursor := c.inputs[c.focusIndex].Cursor()
199200 if cursor != nil {
200201 cursor = c.moveCursor(cursor)
201202 }
202− view.SetCursor(cursor)
203− return view
203+ return cursor
204204}
205205
206206func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
internal/tui/components/dialogs/commands/commands.go+14−8
@@ -143,23 +143,29 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
143143 return c, nil
144144}
145145
146−func (c *commandDialogCmp) View() tea.View {
146+func (c *commandDialogCmp) View() string {
147147 t := styles.CurrentTheme()
148− listView := c.commandList.View()
148+ listView := c.commandList
149149 radio := c.commandTypeRadio()
150150 content := lipgloss.JoinVertical(
151151 lipgloss.Left,
152152 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
153− listView.String(),
153+ listView.View(),
154154 "",
155155 t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
156156 )
157− v := tea.NewView(c.style().Render(content))
158− if listView.Cursor() != nil {
159− c := c.moveCursor(listView.Cursor())
160− v.SetCursor(c)
157+ return c.style().Render(content)
158+}
159+
160+func (c *commandDialogCmp) Cursor() *tea.Cursor {
161+ if cursor, ok := c.commandList.(util.Cursor); ok {
162+ cursor := cursor.Cursor()
163+ if cursor != nil {
164+ cursor = c.moveCursor(cursor)
165+ }
166+ return cursor
161167 }
162− return v
168+ return nil
163169}
164170
165171func (c *commandDialogCmp) commandTypeRadio() string {
internal/tui/components/dialogs/commands/item.go+2−2
@@ -35,12 +35,12 @@ func (m *itemSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
3535 return m, nil
3636}
3737
38−func (m *itemSectionModel) View() tea.View {
38+func (m *itemSectionModel) View() string {
3939 t := styles.CurrentTheme()
4040 title := ansi.Truncate(m.title, m.width-2, "…")
4141 style := t.S().Base.Padding(1, 1, 0, 1)
4242 title = t.S().Muted.Render(title)
43− return tea.NewView(style.Render(core.Section(title, m.width-2)))
43+ return style.Render(core.Section(title, m.width-2))
4444}
4545
4646func (m *itemSectionModel) GetSize() (int, int) {
internal/tui/components/dialogs/compact/compact.go+2−2
@@ -242,8 +242,8 @@ func (c *compactDialogCmp) render() string {
242242 Render(dialogContent)
243243}
244244
245−func (c *compactDialogCmp) View() tea.View {
246− return tea.NewView(c.render())
245+func (c *compactDialogCmp) View() string {
246+ return c.render()
247247}
248248
249249// SetSize sets the size of the component.
internal/tui/components/dialogs/dialogs.go+4−5
@@ -37,7 +37,7 @@ type DialogCmp interface {
3737 Dialogs() []DialogModel
3838 HasDialogs() bool
3939 GetLayers() []*lipgloss.Layer
40− ActiveView() *tea.View
40+ ActiveModel() util.Model
4141 ActiveDialogID() DialogID
4242}
4343
@@ -132,12 +132,11 @@ func (d dialogCmp) Dialogs() []DialogModel {
132132 return d.dialogs
133133}
134134
135−func (d dialogCmp) ActiveView() *tea.View {
135+func (d dialogCmp) ActiveModel() util.Model {
136136 if len(d.dialogs) == 0 {
137137 return nil
138138 }
139− view := d.dialogs[len(d.dialogs)-1].View()
140− return &view
139+ return d.dialogs[len(d.dialogs)-1]
141140}
142141
143142func (d dialogCmp) ActiveDialogID() DialogID {
@@ -150,7 +149,7 @@ func (d dialogCmp) ActiveDialogID() DialogID {
150149func (d dialogCmp) GetLayers() []*lipgloss.Layer {
151150 layers := []*lipgloss.Layer{}
152151 for _, dialog := range d.Dialogs() {
153− dialogView := dialog.View().String()
152+ dialogView := dialog.View()
154153 row, col := dialog.Position()
155154 layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
156155 }
internal/tui/components/dialogs/filepicker/filepicker.go+2−2
@@ -148,7 +148,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
148148 return m, tea.Batch(cmds...)
149149}
150150
151−func (m *model) View() tea.View {
151+func (m *model) View() string {
152152 t := styles.CurrentTheme()
153153
154154 content := lipgloss.JoinVertical(
@@ -158,7 +158,7 @@ func (m *model) View() tea.View {
158158 m.filePicker.View(),
159159 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
160160 )
161− return tea.NewView(m.style().Render(content))
161+ return m.style().Render(content)
162162}
163163
164164func (m *model) currentImage() string {
internal/tui/components/dialogs/init/init.go+2−2
@@ -147,8 +147,8 @@ func (m *initDialogCmp) render() string {
147147}
148148
149149// View implements tea.Model.
150−func (m *initDialogCmp) View() tea.View {
151− return tea.NewView(m.render())
150+func (m *initDialogCmp) View() string {
151+ return m.render()
152152}
153153
154154// SetSize sets the size of the component.
internal/tui/components/dialogs/models/models.go+13−7
@@ -154,22 +154,28 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154154 return m, nil
155155}
156156
157−func (m *modelDialogCmp) View() tea.View {
157+func (m *modelDialogCmp) View() string {
158158 t := styles.CurrentTheme()
159159 listView := m.modelList.View()
160160 content := lipgloss.JoinVertical(
161161 lipgloss.Left,
162162 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-4)),
163− listView.String(),
163+ listView,
164164 "",
165165 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
166166 )
167− v := tea.NewView(m.style().Render(content))
168− if listView.Cursor() != nil {
169− c := m.moveCursor(listView.Cursor())
170− v.SetCursor(c)
167+ return m.style().Render(content)
168+}
169+
170+func (m *modelDialogCmp) Cursor() *tea.Cursor {
171+ if cursor, ok := m.modelList.(util.Cursor); ok {
172+ cursor := cursor.Cursor()
173+ if cursor != nil {
174+ cursor = m.moveCursor(cursor)
175+ return cursor
176+ }
171177 }
172− return v
178+ return nil
173179}
174180
175181func (m *modelDialogCmp) style() lipgloss.Style {
internal/tui/components/dialogs/permissions/permissions.go+2−2
@@ -478,8 +478,8 @@ func (p *permissionDialogCmp) render() string {
478478 )
479479}
480480
481−func (p *permissionDialogCmp) View() tea.View {
482− return tea.NewView(p.render())
481+func (p *permissionDialogCmp) View() string {
482+ return p.render()
483483}
484484
485485func (p *permissionDialogCmp) SetSize() tea.Cmd {
internal/tui/components/dialogs/quit/quit.go+2−4
@@ -65,7 +65,7 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6565}
6666
6767// View renders the quit dialog with Yes/No buttons.
68−func (q *quitDialogCmp) View() tea.View {
68+func (q *quitDialogCmp) View() string {
6969 t := styles.CurrentTheme()
7070 baseStyle := t.S().Base
7171 yesStyle := t.S().Text
@@ -100,9 +100,7 @@ func (q *quitDialogCmp) View() tea.View {
100100 Border(lipgloss.RoundedBorder()).
101101 BorderForeground(t.BorderFocus)
102102
103− return tea.NewView(
104− quitDialogStyle.Render(content),
105− )
103+ return quitDialogStyle.Render(content)
106104}
107105
108106func (q *quitDialogCmp) Position() (int, int) {
internal/tui/components/dialogs/sessions/sessions.go+13−7
@@ -122,23 +122,29 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
122122 return s, nil
123123}
124124
125−func (s *sessionDialogCmp) View() tea.View {
125+func (s *sessionDialogCmp) View() string {
126126 t := styles.CurrentTheme()
127127 listView := s.sessionsList.View()
128128 content := lipgloss.JoinVertical(
129129 lipgloss.Left,
130130 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
131− listView.String(),
131+ listView,
132132 "",
133133 t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
134134 )
135135
136− v := tea.NewView(s.style().Render(content))
137− if listView.Cursor() != nil {
138− c := s.moveCursor(listView.Cursor())
139− v.SetCursor(c)
136+ return s.style().Render(content)
137+}
138+
139+func (s *sessionDialogCmp) Cursor() *tea.Cursor {
140+ if cursor, ok := s.sessionsList.(util.Cursor); ok {
141+ cursor := cursor.Cursor()
142+ if cursor != nil {
143+ cursor = s.moveCursor(cursor)
144+ }
145+ return cursor
140146 }
141− return v
147+ return nil
142148}
143149
144150func (s *sessionDialogCmp) style() lipgloss.Style {
internal/tui/components/logs/details.go+2−2
@@ -145,7 +145,7 @@ func getRelativeTime(logTime time.Time) string {
145145 }
146146}
147147
148−func (i *detailCmp) View() tea.View {
148+func (i *detailCmp) View() string {
149149 t := styles.CurrentTheme()
150150 style := t.S().Base.
151151 BorderStyle(lipgloss.RoundedBorder()).
@@ -153,7 +153,7 @@ func (i *detailCmp) View() tea.View {
153153 Width(i.width - 2). // Adjust width for border
154154 Height(i.height - 2). // Adjust height for border
155155 Padding(1)
156− return tea.NewView(style.Render(i.viewport.View()))
156+ return style.Render(i.viewport.View())
157157}
158158
159159func (i *detailCmp) GetSize() (int, int) {
internal/tui/components/logs/table.go+2−2
@@ -74,7 +74,7 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
7474 return i, tea.Batch(cmds...)
7575}
7676
77−func (i *tableCmp) View() tea.View {
77+func (i *tableCmp) View() string {
7878 t := styles.CurrentTheme()
7979 defaultStyles := table.DefaultStyles()
8080
@@ -97,7 +97,7 @@ func (i *tableCmp) View() tea.View {
9797 Foreground(t.FgBase)
9898
9999 i.table.SetStyles(defaultStyles)
100− return tea.NewView(i.table.View())
100+ return i.table.View()
101101}
102102
103103func (i *tableCmp) GetSize() (int, int) {
internal/tui/page/chat/chat.go+12−7
@@ -306,7 +306,7 @@ func (p *chatPage) GetSize() (int, int) {
306306 return p.layout.GetSize()
307307}
308308
309−func (p *chatPage) View() tea.View {
309+func (p *chatPage) View() string {
310310 if !p.compactMode || p.session.ID == "" {
311311 // If not in compact mode or there is no session, we don't show the header
312312 return p.layout.View()
@@ -314,8 +314,8 @@ func (p *chatPage) View() tea.View {
314314 layoutView := p.layout.View()
315315 chatView := strings.Join(
316316 []string{
317− p.header.View().String(),
318− layoutView.String(),
317+ p.header.View(),
318+ layoutView,
319319 }, "\n",
320320 )
321321 layers := []*lipgloss.Layer{
@@ -330,7 +330,7 @@ func (p *chatPage) View() tea.View {
330330 details := style.Render(
331331 lipgloss.JoinVertical(
332332 lipgloss.Left,
333− p.compactSidebar.View().String(),
333+ p.compactSidebar.View(),
334334 version,
335335 ),
336336 )
@@ -339,9 +339,14 @@ func (p *chatPage) View() tea.View {
339339 canvas := lipgloss.NewCanvas(
340340 layers...,
341341 )
342− view := tea.NewView(canvas.Render())
343− view.SetCursor(layoutView.Cursor())
344− return view
342+ return canvas.Render()
343+}
344+
345+func (p *chatPage) Cursor() *tea.Cursor {
346+ if v, ok := p.layout.(util.Cursor); ok {
347+ return v.Cursor()
348+ }
349+ return nil
345350}
346351
347352func (p *chatPage) Bindings() []key.Binding {
internal/tui/page/logs/logs.go+6−8
@@ -51,18 +51,16 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5151 return p, tea.Batch(cmds...)
5252}
5353
54−func (p *logsPage) View() tea.View {
54+func (p *logsPage) View() string {
5555 baseStyle := styles.CurrentTheme().S().Base
5656 style := baseStyle.Width(p.width).Height(p.height).Padding(1)
5757 title := core.Title("Logs", p.width-2)
5858
59− return tea.NewView(
60− style.Render(
61− lipgloss.JoinVertical(lipgloss.Top,
62− title,
63− p.details.View().String(),
64− p.table.View().String(),
65− ),
59+ return style.Render(
60+ lipgloss.JoinVertical(lipgloss.Top,
61+ title,
62+ p.details.View(),
63+ p.table.View(),
6664 ),
6765 )
6866}
internal/tui/tui.go+19−9
@@ -84,6 +84,9 @@ func (a appModel) Init() tea.Cmd {
8484 return nil
8585 })
8686
87+ // Enable mouse support.
88+ cmds = append(cmds, tea.EnableMouseAllMotion)
89+
8790 return tea.Batch(cmds...)
8891}
8992
@@ -391,9 +394,9 @@ func (a *appModel) View() tea.View {
391394 a.status.SetKeyMap(a.keyMap)
392395 pageView := page.View()
393396 components := []string{
394− pageView.String(),
397+ pageView,
395398 }
396− components = append(components, a.status.View().String())
399+ components = append(components, a.status.View())
397400
398401 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
399402 layers := []*lipgloss.Layer{
@@ -406,14 +409,20 @@ func (a *appModel) View() tea.View {
406409 )
407410 }
408411
409− cursor := pageView.Cursor()
410− activeView := a.dialog.ActiveView()
412+ var cursor *tea.Cursor
413+ if v, ok := page.(util.Cursor); ok {
414+ cursor = v.Cursor()
415+ }
416+ activeView := a.dialog.ActiveModel()
411417 if activeView != nil {
412− cursor = activeView.Cursor()
418+ cursor = nil // Reset cursor if a dialog is active unless it implements util.Cursor
419+ if v, ok := activeView.(util.Cursor); ok {
420+ cursor = v.Cursor()
421+ }
413422 }
414423
415424 if a.completions.Open() && cursor != nil {
416− cmp := a.completions.View().String()
425+ cmp := a.completions.View()
417426 x, y := a.completions.Position()
418427 layers = append(
419428 layers,
@@ -425,10 +434,11 @@ func (a *appModel) View() tea.View {
425434 layers...,
426435 )
427436
437+ var view tea.View
428438 t := styles.CurrentTheme()
429− view := tea.NewView(canvas.Render())
430− view.SetBackgroundColor(t.BgBase)
431− view.SetCursor(cursor)
439+ view.Layer = canvas
440+ view.BackgroundColor = t.BgBase
441+ view.Cursor = cursor
432442 return view
433443}
434444
internal/tui/util/util.go+5−1
@@ -6,9 +6,13 @@ import (
66 tea "github.com/charmbracelet/bubbletea/v2"
77)
88
9+type Cursor interface {
10+ Cursor() *tea.Cursor
11+}
12+
913type Model interface {
1014 tea.Model
11− tea.Viewable
15+ tea.ViewModel
1216}
1317
1418func CmdHandler(msg tea.Msg) tea.Cmd {