on main help sign in
refactor: use uv bubbletea
187e915Ayman Bagabas
go.mod+14−4
@@ -1,6 +1,12 @@
1module github.com/charmbracelet/crush
1module github.com/charmbracelet/crush
2
2
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
4
10
5require (
11require (
6 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
12 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
@@ -17,7 +23,7 @@ require (
17 github.com/charmbracelet/fang v0.1.0
23 github.com/charmbracelet/fang v0.1.0
18 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
24 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe
19 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
25 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
21 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444
27 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444
22 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
28 github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
23 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
29 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
@@ -41,7 +47,11 @@ require (
41)
47)
42
48
43require (
49require (
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 (
45 cloud.google.com/go v0.116.0 // indirect
55 cloud.google.com/go v0.116.0 // indirect
46 cloud.google.com/go/auth v0.13.0 // indirect
56 cloud.google.com/go/auth v0.13.0 // indirect
47 cloud.google.com/go/compute/metadata v0.6.0 // indirect
57 cloud.google.com/go/compute/metadata v0.6.0 // indirect
@@ -69,7 +79,6 @@ require (
69 github.com/charmbracelet/colorprofile v0.3.1 // indirect
79 github.com/charmbracelet/colorprofile v0.3.1 // indirect
70 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
80 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
71 github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
81 github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
72− github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
73 github.com/charmbracelet/x/term v0.2.1 // indirect
82 github.com/charmbracelet/x/term v0.2.1 // indirect
74 github.com/charmbracelet/x/windows v0.2.1 // indirect
83 github.com/charmbracelet/x/windows v0.2.1 // indirect
75 github.com/davecgh/go-spew v1.1.1 // indirect
84 github.com/davecgh/go-spew v1.1.1 // indirect
@@ -132,6 +141,7 @@ require (
132 golang.org/x/net v0.39.0 // indirect
141 golang.org/x/net v0.39.0 // indirect
133 golang.org/x/sync v0.13.0 // indirect
142 golang.org/x/sync v0.13.0 // indirect
134 golang.org/x/sys v0.32.0 // indirect
143 golang.org/x/sys v0.32.0 // indirect
144+ golang.org/x/term v0.31.0 // indirect
135 golang.org/x/text v0.24.0 // indirect
145 golang.org/x/text v0.24.0 // indirect
136 google.golang.org/genai v1.3.0
146 google.golang.org/genai v1.3.0
137 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
147 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
70github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
70github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
71github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
71github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
72github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
72github.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=
75github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
73github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
76github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
74github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
77github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg=
75github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg=
78github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
76github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
79github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
77github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
80github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
78github.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=
79+github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
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=
80+github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
85github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
81github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
86github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
82github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
87github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0=
83github.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
90github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
86github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
91github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef h1:v7qwsZ2OxzlwvpKwz8dtZXp7fIJlcDEUOyFBNE4fz4Q=
87github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef h1:v7qwsZ2OxzlwvpKwz8dtZXp7fIJlcDEUOyFBNE4fz4Q=
92github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
88github.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=
95github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
89github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
96github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
90github.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=
97github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
93github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
98github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
94github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
99github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
95github.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) {
257}
257}
258
258
259// View renders the animation.
259// View renders the animation.
260−func (a anim) View() tea.View {
260+func (a anim) View() string {
261 var (
261 var (
262 t = styles.CurrentTheme()
262 t = styles.CurrentTheme()
263 b strings.Builder
263 b strings.Builder
@@ -289,7 +289,7 @@ func (a anim) View() tea.View {
289 b.WriteString(textStyle.Render(a.ellipsis.View()))
289 b.WriteString(textStyle.Render(a.ellipsis.View()))
290 }
290 }
291
291
292− return tea.NewView(b.String())
292+ return b.String()
293}
293}
294
294
295func GetColor(c color.Color) string {
295func 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) {
101}
101}
102
102
103// View renders the message list or an initial screen if empty.
103// View renders the message list or an initial screen if empty.
104−func (m *messageListCmp) View() tea.View {
104+func (m *messageListCmp) View() string {
105− return tea.NewView(
106− lipgloss.JoinVertical(
105+ return lipgloss.JoinVertical(
107− lipgloss.Left,
106+ lipgloss.Left,
108− m.listCmp.View().String(),
107+ m.listCmp.View(),
109− ),
110 )
108 )
111}
109}
112
110
internal/tui/components/chat/editor/editor.go+8−8
@@ -265,20 +265,22 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
265 return m, tea.Batch(cmds...)
265 return m, tea.Batch(cmds...)
266}
266}
267
267
268−func (m *editorCmp) View() tea.View {
268+func (m *editorCmp) Cursor() *tea.Cursor {
269− t := styles.CurrentTheme()
270 cursor := m.textarea.Cursor()
269 cursor := m.textarea.Cursor()
271 if cursor != nil {
270 if cursor != nil {
272 cursor.X = cursor.X + m.x + 1
271 cursor.X = cursor.X + m.x + 1
273 cursor.Y = cursor.Y + m.y + 1 // adjust for padding
272 cursor.Y = cursor.Y + m.y + 1 // adjust for padding
274 }
273 }
274+ return cursor
275+}
276+
277+func (m *editorCmp) View() string {
278+ t := styles.CurrentTheme()
275 if len(m.attachments) == 0 {
279 if len(m.attachments) == 0 {
276 content := t.S().Base.Padding(1).Render(
280 content := t.S().Base.Padding(1).Render(
277 m.textarea.View(),
281 m.textarea.View(),
278 )
282 )
279− view := tea.NewView(content)
280− view.SetCursor(cursor)
281− return view
283+ return content
282 }
284 }
283 content := t.S().Base.Padding(0, 1, 1, 1).Render(
285 content := t.S().Base.Padding(0, 1, 1, 1).Render(
284 lipgloss.JoinVertical(lipgloss.Top,
286 lipgloss.JoinVertical(lipgloss.Top,
@@ -286,9 +288,7 @@ func (m *editorCmp) View() tea.View {
286 m.textarea.View(),
288 m.textarea.View(),
287 ),
289 ),
288 )
290 )
289− view := tea.NewView(content)
290− view.SetCursor(cursor)
291− return view
291+ return content
292}
292}
293
293
294func (m *editorCmp) SetSize(width, height int) tea.Cmd {
294func (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) {
58 return p, nil
58 return p, nil
59}
59}
60
60
61−func (p *header) View() tea.View {
61+func (p *header) View() string {
62 if p.session.ID == "" {
62 if p.session.ID == "" {
63− return tea.NewView("")
63+ return ""
64 }
64 }
65
65
66 t := styles.CurrentTheme()
66 t := styles.CurrentTheme()
@@ -87,7 +87,7 @@ func (p *header) View() tea.View {
87 parts...,
87 parts...,
88 ),
88 ),
89 )
89 )
90− return tea.NewView(content)
90+ return content
91}
91}
92
92
93func (h *header) details() string {
93func (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) {
83
83
84// View renders the message component based on its current state.
84// View renders the message component based on its current state.
85// Returns different views for spinning, user, and assistant messages.
85// Returns different views for spinning, user, and assistant messages.
86−func (m *messageCmp) View() tea.View {
86+func (m *messageCmp) View() string {
87 if m.spinning {
87 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())
89 }
89 }
90 if m.message.ID != "" {
90 if m.message.ID != "" {
91 // this is a user or assistant message
91 // this is a user or assistant message
92 switch m.message.Role {
92 switch m.message.Role {
93 case message.User:
93 case message.User:
94− return tea.NewView(m.renderUserMessage())
94+ return m.renderUserMessage()
95 default:
95 default:
96− return tea.NewView(m.renderAssistantMessage())
96+ return m.renderAssistantMessage()
97 }
97 }
98 }
98 }
99− return tea.NewView(m.style().Render("No message content"))
99+ return m.style().Render("No message content")
100}
100}
101
101
102// GetMessage returns the underlying message data
102// GetMessage returns the underlying message data
@@ -283,7 +283,7 @@ func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) {
283 return m, nil
283 return m, nil
284}
284}
285
285
286−func (m *assistantSectionModel) View() tea.View {
286+func (m *assistantSectionModel) View() string {
287 t := styles.CurrentTheme()
287 t := styles.CurrentTheme()
288 finishData := m.message.FinishPart()
288 finishData := m.message.FinishPart()
289 finishTime := time.Unix(finishData.Time, 0)
289 finishTime := time.Unix(finishData.Time, 0)
@@ -292,10 +292,8 @@ func (m *assistantSectionModel) View() tea.View {
292 icon := t.S().Subtle.Render(styles.ModelIcon)
292 icon := t.S().Subtle.Render(styles.ModelIcon)
293 model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
293 model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name)
294 assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
294 assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg)
295− return tea.NewView(
296− t.S().Base.PaddingLeft(2).Render(
295+ return t.S().Base.PaddingLeft(2).Render(
297− core.Section(assistant, m.width-2),
296+ core.Section(assistant, m.width-2),
298− ),
299 )
297 )
300}
298}
301
299
internal/tui/components/chat/messages/renderer.go+1−1
@@ -542,7 +542,7 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
542
542
543 if v.result.ToolCallID == "" {
543 if v.result.ToolCallID == "" {
544 v.spinning = true
544 v.spinning = true
545− parts = append(parts, v.anim.View().String())
545+ parts = append(parts, v.anim.View())
546 } else {
546 } else {
547 v.spinning = false
547 v.spinning = false
548 }
548 }
internal/tui/components/chat/messages/tool.go+4−4
@@ -133,19 +133,19 @@ func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
133
133
134// View renders the tool call component based on its current state.
134// View renders the tool call component based on its current state.
135// Shows either a pending animation or the tool-specific rendered result.
135// Shows either a pending animation or the tool-specific rendered result.
136−func (m *toolCallCmp) View() tea.View {
136+func (m *toolCallCmp) View() string {
137 box := m.style()
137 box := m.style()
138
138
139 if !m.call.Finished && !m.cancelled {
139 if !m.call.Finished && !m.cancelled {
140− return tea.NewView(box.Render(m.renderPending()))
140+ return box.Render(m.renderPending())
141 }
141 }
142
142
143 r := registry.lookup(m.call.Name)
143 r := registry.lookup(m.call.Name)
144
144
145 if m.isNested {
145 if m.isNested {
146− return tea.NewView(box.Render(r.Render(m)))
146+ return box.Render(r.Render(m))
147 }
147 }
148− return tea.NewView(box.Render(r.Render(m)))
148+ return box.Render(r.Render(m))
149}
149}
150
150
151// State management methods
151// 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) {
107 return m, nil
107 return m, nil
108}
108}
109
109
110−func (m *sidebarCmp) View() tea.View {
110+func (m *sidebarCmp) View() string {
111 t := styles.CurrentTheme()
111 t := styles.CurrentTheme()
112 parts := []string{}
112 parts := []string{}
113 if !m.compactMode {
113 if !m.compactMode {
@@ -139,9 +139,7 @@ func (m *sidebarCmp) View() tea.View {
139 m.mcpBlock(),
139 m.mcpBlock(),
140 )
140 )
141
141
142− return tea.NewView(
143− lipgloss.JoinVertical(lipgloss.Left, parts...),
142+ return lipgloss.JoinVertical(lipgloss.Left, parts...)
144− )
145}
143}
146
144
147func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
145func (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) {
157}
157}
158
158
159// View implements Completions.
159// View implements Completions.
160−func (c *completionsCmp) View() tea.View {
160+func (c *completionsCmp) View() string {
161 if len(c.list.Items()) == 0 {
161 if len(c.list.Items()) == 0 {
162− return tea.NewView(c.style().Render("No completions found"))
162+ return c.style().Render("No completions found")
163 }
163 }
164
164
165− view := tea.NewView(
166− c.style().Render(c.list.View().String()),
165+ return c.style().Render(c.list.View())
167− )
168− return view
169}
166}
170
167
171func (c *completionsCmp) style() lipgloss.Style {
168func (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) {
75}
75}
76
76
77// View implements CommandItem.
77// View implements CommandItem.
78−func (c *completionItemCmp) View() tea.View {
78+func (c *completionItemCmp) View() string {
79 t := styles.CurrentTheme()
79 t := styles.CurrentTheme()
80
80
81 itemStyle := t.S().Base.Padding(0, 1).Width(c.width)
81 itemStyle := t.S().Base.Padding(0, 1).Width(c.width)
@@ -135,7 +135,7 @@ func (c *completionItemCmp) View() tea.View {
135 parts...,
135 parts...,
136 ),
136 ),
137 )
137 )
138− return tea.NewView(item)
138+ return item
139}
139}
140
140
141// Blur implements CommandItem.
141// 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) {
72 }
72 }
73}
73}
74
74
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 {
76 t := styles.CurrentTheme()
83 t := styles.CurrentTheme()
77 width := c.width
84 width := c.width
78 height := c.height
85 height := c.height
@@ -106,10 +113,7 @@ func (c *container) View() tea.View {
106 PaddingLeft(c.paddingLeft)
113 PaddingLeft(c.paddingLeft)
107
114
108 contentView := c.content.View()
115 contentView := c.content.View()
109− view := tea.NewView(style.Render(contentView.String()))
116+ return style.Render(contentView)
110− cursor := contentView.Cursor()
111− view.SetCursor(cursor)
112− return view
113}
117}
114
118
115func (c *container) SetSize(width, height int) tea.Cmd {
119func (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) {
104 return s, tea.Batch(cmds...)
104 return s, tea.Batch(cmds...)
105}
105}
106
106
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 {
108 var topSection string
125 var topSection string
109
126
110 if s.leftPanel != nil && s.rightPanel != nil {
127 if s.leftPanel != nil && s.rightPanel != nil {
111 leftView := s.leftPanel.View()
128 leftView := s.leftPanel.View()
112 rightView := s.rightPanel.View()
129 rightView := s.rightPanel.View()
113− topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView.String(), rightView.String())
130+ topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
114 } else if s.leftPanel != nil {
131 } else if s.leftPanel != nil {
115− topSection = s.leftPanel.View().String()
132+ topSection = s.leftPanel.View()
116 } else if s.rightPanel != nil {
133 } else if s.rightPanel != nil {
117− topSection = s.rightPanel.View().String()
134+ topSection = s.rightPanel.View()
118 } else {
135 } else {
119 topSection = ""
136 topSection = ""
120 }
137 }
@@ -123,32 +140,20 @@ func (s *splitPaneLayout) View() tea.View {
123
140
124 if s.bottomPanel != nil && topSection != "" {
141 if s.bottomPanel != nil && topSection != "" {
125 bottomView := s.bottomPanel.View()
142 bottomView := s.bottomPanel.View()
126− finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView.String())
143+ finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
127 } else if s.bottomPanel != nil {
144 } else if s.bottomPanel != nil {
128− finalView = s.bottomPanel.View().String()
145+ finalView = s.bottomPanel.View()
129 } else {
146 } else {
130 finalView = topSection
147 finalView = topSection
131 }
148 }
132
149
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−
143 t := styles.CurrentTheme()
150 t := styles.CurrentTheme()
144
151
145 style := t.S().Base.
152 style := t.S().Base.
146 Width(s.width).
153 Width(s.width).
147 Height(s.height)
154 Height(s.height)
148
155
149− view := tea.NewView(style.Render(finalView))
156+ return style.Render(finalView)
150− view.SetCursor(cursor)
151− return view
152}
157}
153
158
154func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
159func (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) {
281 return m, nil
281 return m, nil
282}
282}
283
283
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+
284// View renders the list to a string for display.
292// View renders the list to a string for display.
285// Returns empty string if the list has no dimensions.
293// Returns empty string if the list has no dimensions.
286// Triggers re-rendering if needed before returning content.
294// Triggers re-rendering if needed before returning content.
287−func (m *model) View() tea.View {
295+func (m *model) View() string {
288 if m.viewState.height == 0 || m.viewState.width == 0 {
296 if m.viewState.height == 0 || m.viewState.width == 0 {
289− return tea.NewView("") // No content to display
297+ return "" // No content to display
290 }
298 }
291 if m.renderState.needsRerender {
299 if m.renderState.needsRerender {
292 m.renderVisible()
300 m.renderVisible()
@@ -304,11 +312,7 @@ func (m *model) View() tea.View {
304 content,
312 content,
305 )
313 )
306 }
314 }
307− view := tea.NewView(content)
315+ return content
308− if m.filterable && !m.hideFilterInput {
309− view.SetCursor(m.input.Cursor())
310− }
311− return view
312}
316}
313
317
314// handleKeyPress processes keyboard input for list navigation.
318// handleKeyPress processes keyboard input for list navigation.
@@ -834,7 +838,7 @@ func (m *model) rerenderItem(inx int) {
834func (m *model) getItemLines(item util.Model) []string {
838func (m *model) getItemLines(item util.Model) []string {
835 var itemLines []string
839 var itemLines []string
836
840
837− itemLines = strings.Split(item.View().String(), "\n")
841+ itemLines = strings.Split(item.View(), "\n")
838
842
839 if m.gapSize > 0 {
843 if m.gapSize > 0 {
840 gap := make([]string, m.gapSize)
844 gap := make([]string, m.gapSize)
@@ -1262,7 +1266,7 @@ func (m *model) filterSection(sect section, search string) *section {
1262
1266
1263 // Check if section header itself matches
1267 // Check if section header itself matches
1264 if sect.header != nil {
1268 if sect.header != nil {
1265− headerText := strings.ToLower(sect.header.View().String())
1269+ headerText := strings.ToLower(sect.header.View())
1266 if strings.Contains(headerText, search) {
1270 if strings.Contains(headerText, search) {
1267 hasHeaderMatch = true
1271 hasHeaderMatch = true
1268 // If header matches, include all items in the section
1272 // 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) {
94 return m, nil
94 return m, nil
95}
95}
96
96
97−func (m *statusCmp) View() tea.View {
97+func (m *statusCmp) View() string {
98 t := styles.CurrentTheme()
98 t := styles.CurrentTheme()
99 status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
99 status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap))
100 if m.info.Msg != "" {
100 if m.info.Msg != "" {
101 status = m.infoMsg()
101 status = m.infoMsg()
102 }
102 }
103− return tea.NewView(status)
103+ return status
104}
104}
105
105
106func (m *statusCmp) infoMsg() string {
106func (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) {
139}
139}
140
140
141// View implements CommandArgumentsDialog.
141// View implements CommandArgumentsDialog.
142−func (c *commandArgumentsDialogCmp) View() tea.View {
142+func (c *commandArgumentsDialogCmp) View() string {
143 t := styles.CurrentTheme()
143 t := styles.CurrentTheme()
144 baseStyle := t.S().Base
144 baseStyle := t.S().Base
145
145
@@ -188,19 +188,19 @@ func (c *commandArgumentsDialogCmp) View() tea.View {
188 elements...,
188 elements...,
189 )
189 )
190
190
191− view := tea.NewView(
192− baseStyle.Padding(1, 1, 0, 1).
191+ return baseStyle.Padding(1, 1, 0, 1).
193− Border(lipgloss.RoundedBorder()).
192+ Border(lipgloss.RoundedBorder()).
194− BorderForeground(t.BorderFocus).
193+ BorderForeground(t.BorderFocus).
195− Width(c.width).
194+ Width(c.width).
196− Render(content),
195+ Render(content)
197− )
196+}
197+
198+func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
198 cursor := c.inputs[c.focusIndex].Cursor()
199 cursor := c.inputs[c.focusIndex].Cursor()
199 if cursor != nil {
200 if cursor != nil {
200 cursor = c.moveCursor(cursor)
201 cursor = c.moveCursor(cursor)
201 }
202 }
202− view.SetCursor(cursor)
203− return view
203+ return cursor
204}
204}
205
205
206func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
206func (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) {
143 return c, nil
143 return c, nil
144}
144}
145
145
146−func (c *commandDialogCmp) View() tea.View {
146+func (c *commandDialogCmp) View() string {
147 t := styles.CurrentTheme()
147 t := styles.CurrentTheme()
148− listView := c.commandList.View()
148+ listView := c.commandList
149 radio := c.commandTypeRadio()
149 radio := c.commandTypeRadio()
150 content := lipgloss.JoinVertical(
150 content := lipgloss.JoinVertical(
151 lipgloss.Left,
151 lipgloss.Left,
152 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Commands", c.width-lipgloss.Width(radio)-5)+" "+radio),
152 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(),
154 "",
154 "",
155 t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
155 t.S().Base.Width(c.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(c.help.View(c.keyMap)),
156 )
156 )
157− v := tea.NewView(c.style().Render(content))
157+ return c.style().Render(content)
158− if listView.Cursor() != nil {
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 {
159− c := c.moveCursor(listView.Cursor())
164+ cursor = c.moveCursor(cursor)
160− v.SetCursor(c)
165+ }
166+ return cursor
161 }
167 }
162− return v
168+ return nil
163}
169}
164
170
165func (c *commandDialogCmp) commandTypeRadio() string {
171func (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) {
35 return m, nil
35 return m, nil
36}
36}
37
37
38−func (m *itemSectionModel) View() tea.View {
38+func (m *itemSectionModel) View() string {
39 t := styles.CurrentTheme()
39 t := styles.CurrentTheme()
40 title := ansi.Truncate(m.title, m.width-2, "…")
40 title := ansi.Truncate(m.title, m.width-2, "…")
41 style := t.S().Base.Padding(1, 1, 0, 1)
41 style := t.S().Base.Padding(1, 1, 0, 1)
42 title = t.S().Muted.Render(title)
42 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))
44}
44}
45
45
46func (m *itemSectionModel) GetSize() (int, int) {
46func (m *itemSectionModel) GetSize() (int, int) {
internal/tui/components/dialogs/compact/compact.go+2−2
@@ -242,8 +242,8 @@ func (c *compactDialogCmp) render() string {
242 Render(dialogContent)
242 Render(dialogContent)
243}
243}
244
244
245−func (c *compactDialogCmp) View() tea.View {
245+func (c *compactDialogCmp) View() string {
246− return tea.NewView(c.render())
246+ return c.render()
247}
247}
248
248
249// SetSize sets the size of the component.
249// SetSize sets the size of the component.
internal/tui/components/dialogs/dialogs.go+4−5
@@ -37,7 +37,7 @@ type DialogCmp interface {
37 Dialogs() []DialogModel
37 Dialogs() []DialogModel
38 HasDialogs() bool
38 HasDialogs() bool
39 GetLayers() []*lipgloss.Layer
39 GetLayers() []*lipgloss.Layer
40− ActiveView() *tea.View
40+ ActiveModel() util.Model
41 ActiveDialogID() DialogID
41 ActiveDialogID() DialogID
42}
42}
43
43
@@ -132,12 +132,11 @@ func (d dialogCmp) Dialogs() []DialogModel {
132 return d.dialogs
132 return d.dialogs
133}
133}
134
134
135−func (d dialogCmp) ActiveView() *tea.View {
135+func (d dialogCmp) ActiveModel() util.Model {
136 if len(d.dialogs) == 0 {
136 if len(d.dialogs) == 0 {
137 return nil
137 return nil
138 }
138 }
139− view := d.dialogs[len(d.dialogs)-1].View()
139+ return d.dialogs[len(d.dialogs)-1]
140− return &view
141}
140}
142
141
143func (d dialogCmp) ActiveDialogID() DialogID {
142func (d dialogCmp) ActiveDialogID() DialogID {
@@ -150,7 +149,7 @@ func (d dialogCmp) ActiveDialogID() DialogID {
150func (d dialogCmp) GetLayers() []*lipgloss.Layer {
149func (d dialogCmp) GetLayers() []*lipgloss.Layer {
151 layers := []*lipgloss.Layer{}
150 layers := []*lipgloss.Layer{}
152 for _, dialog := range d.Dialogs() {
151 for _, dialog := range d.Dialogs() {
153− dialogView := dialog.View().String()
152+ dialogView := dialog.View()
154 row, col := dialog.Position()
153 row, col := dialog.Position()
155 layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
154 layers = append(layers, lipgloss.NewLayer(dialogView).X(col).Y(row))
156 }
155 }
internal/tui/components/dialogs/filepicker/filepicker.go+2−2
@@ -148,7 +148,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
148 return m, tea.Batch(cmds...)
148 return m, tea.Batch(cmds...)
149}
149}
150
150
151−func (m *model) View() tea.View {
151+func (m *model) View() string {
152 t := styles.CurrentTheme()
152 t := styles.CurrentTheme()
153
153
154 content := lipgloss.JoinVertical(
154 content := lipgloss.JoinVertical(
@@ -158,7 +158,7 @@ func (m *model) View() tea.View {
158 m.filePicker.View(),
158 m.filePicker.View(),
159 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
159 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
160 )
160 )
161− return tea.NewView(m.style().Render(content))
161+ return m.style().Render(content)
162}
162}
163
163
164func (m *model) currentImage() string {
164func (m *model) currentImage() string {
internal/tui/components/dialogs/init/init.go+2−2
@@ -147,8 +147,8 @@ func (m *initDialogCmp) render() string {
147}
147}
148
148
149// View implements tea.Model.
149// View implements tea.Model.
150−func (m *initDialogCmp) View() tea.View {
150+func (m *initDialogCmp) View() string {
151− return tea.NewView(m.render())
151+ return m.render()
152}
152}
153
153
154// SetSize sets the size of the component.
154// 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) {
154 return m, nil
154 return m, nil
155}
155}
156
156
157−func (m *modelDialogCmp) View() tea.View {
157+func (m *modelDialogCmp) View() string {
158 t := styles.CurrentTheme()
158 t := styles.CurrentTheme()
159 listView := m.modelList.View()
159 listView := m.modelList.View()
160 content := lipgloss.JoinVertical(
160 content := lipgloss.JoinVertical(
161 lipgloss.Left,
161 lipgloss.Left,
162 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-4)),
162 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Model", m.width-4)),
163− listView.String(),
163+ listView,
164 "",
164 "",
165 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
165 t.S().Base.Width(m.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(m.help.View(m.keyMap)),
166 )
166 )
167− v := tea.NewView(m.style().Render(content))
167+ return m.style().Render(content)
168− if listView.Cursor() != nil {
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 {
169− c := m.moveCursor(listView.Cursor())
174+ cursor = m.moveCursor(cursor)
170− v.SetCursor(c)
175+ return cursor
176+ }
171 }
177 }
172− return v
178+ return nil
173}
179}
174
180
175func (m *modelDialogCmp) style() lipgloss.Style {
181func (m *modelDialogCmp) style() lipgloss.Style {
internal/tui/components/dialogs/permissions/permissions.go+2−2
@@ -478,8 +478,8 @@ func (p *permissionDialogCmp) render() string {
478 )
478 )
479}
479}
480
480
481−func (p *permissionDialogCmp) View() tea.View {
481+func (p *permissionDialogCmp) View() string {
482− return tea.NewView(p.render())
482+ return p.render()
483}
483}
484
484
485func (p *permissionDialogCmp) SetSize() tea.Cmd {
485func (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) {
65}
65}
66
66
67// View renders the quit dialog with Yes/No buttons.
67// View renders the quit dialog with Yes/No buttons.
68−func (q *quitDialogCmp) View() tea.View {
68+func (q *quitDialogCmp) View() string {
69 t := styles.CurrentTheme()
69 t := styles.CurrentTheme()
70 baseStyle := t.S().Base
70 baseStyle := t.S().Base
71 yesStyle := t.S().Text
71 yesStyle := t.S().Text
@@ -100,9 +100,7 @@ func (q *quitDialogCmp) View() tea.View {
100 Border(lipgloss.RoundedBorder()).
100 Border(lipgloss.RoundedBorder()).
101 BorderForeground(t.BorderFocus)
101 BorderForeground(t.BorderFocus)
102
102
103− return tea.NewView(
104− quitDialogStyle.Render(content),
103+ return quitDialogStyle.Render(content)
105− )
106}
104}
107
105
108func (q *quitDialogCmp) Position() (int, int) {
106func (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) {
122 return s, nil
122 return s, nil
123}
123}
124
124
125−func (s *sessionDialogCmp) View() tea.View {
125+func (s *sessionDialogCmp) View() string {
126 t := styles.CurrentTheme()
126 t := styles.CurrentTheme()
127 listView := s.sessionsList.View()
127 listView := s.sessionsList.View()
128 content := lipgloss.JoinVertical(
128 content := lipgloss.JoinVertical(
129 lipgloss.Left,
129 lipgloss.Left,
130 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
130 t.S().Base.Padding(0, 1, 1, 1).Render(core.Title("Switch Session", s.width-4)),
131− listView.String(),
131+ listView,
132 "",
132 "",
133 t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
133 t.S().Base.Width(s.width-2).PaddingLeft(1).AlignHorizontal(lipgloss.Left).Render(s.help.View(s.keyMap)),
134 )
134 )
135
135
136− v := tea.NewView(s.style().Render(content))
136+ return s.style().Render(content)
137− if listView.Cursor() != nil {
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 {
138− c := s.moveCursor(listView.Cursor())
143+ cursor = s.moveCursor(cursor)
139− v.SetCursor(c)
144+ }
145+ return cursor
140 }
146 }
141− return v
147+ return nil
142}
148}
143
149
144func (s *sessionDialogCmp) style() lipgloss.Style {
150func (s *sessionDialogCmp) style() lipgloss.Style {
internal/tui/components/logs/details.go+2−2
@@ -145,7 +145,7 @@ func getRelativeTime(logTime time.Time) string {
145 }
145 }
146}
146}
147
147
148−func (i *detailCmp) View() tea.View {
148+func (i *detailCmp) View() string {
149 t := styles.CurrentTheme()
149 t := styles.CurrentTheme()
150 style := t.S().Base.
150 style := t.S().Base.
151 BorderStyle(lipgloss.RoundedBorder()).
151 BorderStyle(lipgloss.RoundedBorder()).
@@ -153,7 +153,7 @@ func (i *detailCmp) View() tea.View {
153 Width(i.width - 2). // Adjust width for border
153 Width(i.width - 2). // Adjust width for border
154 Height(i.height - 2). // Adjust height for border
154 Height(i.height - 2). // Adjust height for border
155 Padding(1)
155 Padding(1)
156− return tea.NewView(style.Render(i.viewport.View()))
156+ return style.Render(i.viewport.View())
157}
157}
158
158
159func (i *detailCmp) GetSize() (int, int) {
159func (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) {
74 return i, tea.Batch(cmds...)
74 return i, tea.Batch(cmds...)
75}
75}
76
76
77−func (i *tableCmp) View() tea.View {
77+func (i *tableCmp) View() string {
78 t := styles.CurrentTheme()
78 t := styles.CurrentTheme()
79 defaultStyles := table.DefaultStyles()
79 defaultStyles := table.DefaultStyles()
80
80
@@ -97,7 +97,7 @@ func (i *tableCmp) View() tea.View {
97 Foreground(t.FgBase)
97 Foreground(t.FgBase)
98
98
99 i.table.SetStyles(defaultStyles)
99 i.table.SetStyles(defaultStyles)
100− return tea.NewView(i.table.View())
100+ return i.table.View()
101}
101}
102
102
103func (i *tableCmp) GetSize() (int, int) {
103func (i *tableCmp) GetSize() (int, int) {
internal/tui/page/chat/chat.go+12−7
@@ -306,7 +306,7 @@ func (p *chatPage) GetSize() (int, int) {
306 return p.layout.GetSize()
306 return p.layout.GetSize()
307}
307}
308
308
309−func (p *chatPage) View() tea.View {
309+func (p *chatPage) View() string {
310 if !p.compactMode || p.session.ID == "" {
310 if !p.compactMode || p.session.ID == "" {
311 // If not in compact mode or there is no session, we don't show the header
311 // If not in compact mode or there is no session, we don't show the header
312 return p.layout.View()
312 return p.layout.View()
@@ -314,8 +314,8 @@ func (p *chatPage) View() tea.View {
314 layoutView := p.layout.View()
314 layoutView := p.layout.View()
315 chatView := strings.Join(
315 chatView := strings.Join(
316 []string{
316 []string{
317− p.header.View().String(),
317+ p.header.View(),
318− layoutView.String(),
318+ layoutView,
319 }, "\n",
319 }, "\n",
320 )
320 )
321 layers := []*lipgloss.Layer{
321 layers := []*lipgloss.Layer{
@@ -330,7 +330,7 @@ func (p *chatPage) View() tea.View {
330 details := style.Render(
330 details := style.Render(
331 lipgloss.JoinVertical(
331 lipgloss.JoinVertical(
332 lipgloss.Left,
332 lipgloss.Left,
333− p.compactSidebar.View().String(),
333+ p.compactSidebar.View(),
334 version,
334 version,
335 ),
335 ),
336 )
336 )
@@ -339,9 +339,14 @@ func (p *chatPage) View() tea.View {
339 canvas := lipgloss.NewCanvas(
339 canvas := lipgloss.NewCanvas(
340 layers...,
340 layers...,
341 )
341 )
342− view := tea.NewView(canvas.Render())
342+ return canvas.Render()
343− view.SetCursor(layoutView.Cursor())
343+}
344+
345+func (p *chatPage) Cursor() *tea.Cursor {
346+ if v, ok := p.layout.(util.Cursor); ok {
347+ return v.Cursor()
348+ }
344− return view
349+ return nil
345}
350}
346
351
347func (p *chatPage) Bindings() []key.Binding {
352func (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) {
51 return p, tea.Batch(cmds...)
51 return p, tea.Batch(cmds...)
52}
52}
53
53
54−func (p *logsPage) View() tea.View {
54+func (p *logsPage) View() string {
55 baseStyle := styles.CurrentTheme().S().Base
55 baseStyle := styles.CurrentTheme().S().Base
56 style := baseStyle.Width(p.width).Height(p.height).Padding(1)
56 style := baseStyle.Width(p.width).Height(p.height).Padding(1)
57 title := core.Title("Logs", p.width-2)
57 title := core.Title("Logs", p.width-2)
58
58
59− return tea.NewView(
60− style.Render(
59+ return style.Render(
61− lipgloss.JoinVertical(lipgloss.Top,
60+ lipgloss.JoinVertical(lipgloss.Top,
62− title,
61+ title,
63− p.details.View().String(),
62+ p.details.View(),
64− p.table.View().String(),
63+ p.table.View(),
65− ),
66 ),
64 ),
67 )
65 )
68}
66}
internal/tui/tui.go+19−9
@@ -84,6 +84,9 @@ func (a appModel) Init() tea.Cmd {
84 return nil
84 return nil
85 })
85 })
86
86
87+ // Enable mouse support.
88+ cmds = append(cmds, tea.EnableMouseAllMotion)
89+
87 return tea.Batch(cmds...)
90 return tea.Batch(cmds...)
88}
91}
89
92
@@ -391,9 +394,9 @@ func (a *appModel) View() tea.View {
391 a.status.SetKeyMap(a.keyMap)
394 a.status.SetKeyMap(a.keyMap)
392 pageView := page.View()
395 pageView := page.View()
393 components := []string{
396 components := []string{
394− pageView.String(),
397+ pageView,
395 }
398 }
396− components = append(components, a.status.View().String())
399+ components = append(components, a.status.View())
397
400
398 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
401 appView := lipgloss.JoinVertical(lipgloss.Top, components...)
399 layers := []*lipgloss.Layer{
402 layers := []*lipgloss.Layer{
@@ -406,14 +409,20 @@ func (a *appModel) View() tea.View {
406 )
409 )
407 }
410 }
408
411
409− cursor := pageView.Cursor()
412+ var cursor *tea.Cursor
413+ if v, ok := page.(util.Cursor); ok {
414+ cursor = v.Cursor()
415+ }
410− activeView := a.dialog.ActiveView()
416+ activeView := a.dialog.ActiveModel()
411 if activeView != nil {
417 if activeView != nil {
418+ cursor = nil // Reset cursor if a dialog is active unless it implements util.Cursor
412− cursor = activeView.Cursor()
419+ if v, ok := activeView.(util.Cursor); ok {
420+ cursor = v.Cursor()
421+ }
413 }
422 }
414
423
415 if a.completions.Open() && cursor != nil {
424 if a.completions.Open() && cursor != nil {
416− cmp := a.completions.View().String()
425+ cmp := a.completions.View()
417 x, y := a.completions.Position()
426 x, y := a.completions.Position()
418 layers = append(
427 layers = append(
419 layers,
428 layers,
@@ -425,10 +434,11 @@ func (a *appModel) View() tea.View {
425 layers...,
434 layers...,
426 )
435 )
427
436
437+ var view tea.View
428 t := styles.CurrentTheme()
438 t := styles.CurrentTheme()
429− view := tea.NewView(canvas.Render())
439+ view.Layer = canvas
430− view.SetBackgroundColor(t.BgBase)
440+ view.BackgroundColor = t.BgBase
431− view.SetCursor(cursor)
441+ view.Cursor = cursor
432 return view
442 return view
433}
443}
434
444
internal/tui/util/util.go+5−1
@@ -6,9 +6,13 @@ import (
6 tea "github.com/charmbracelet/bubbletea/v2"
6 tea "github.com/charmbracelet/bubbletea/v2"
7)
7)
8
8
9+type Cursor interface {
10+ Cursor() *tea.Cursor
11+}
12+
9type Model interface {
13type Model interface {
10 tea.Model
14 tea.Model
11− tea.Viewable
15+ tea.ViewModel
12}
16}
13
17
14func CmdHandler(msg tea.Msg) tea.Cmd {
18func CmdHandler(msg tea.Msg) tea.Cmd {