on main help sign in
chore: rewrite the chat page

chore: small fixes chore: small fixes

commit1526d1afd904930d2d08074130809ea83097d2c7 authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 2 Jul 2025, 17:57 (1 year ago) parent4e38ae0
9 files changed+486−324
internal/tui/components/chat/chat.go+20−10 internal/tui/components/chat/editor/editor.go+20−6 internal/tui/components/chat/header/header.go+10−7 internal/tui/components/chat/sidebar/sidebar.go+34−29 internal/tui/components/chat/splash/splash.go+19−2 internal/tui/components/core/layout/split.go−5 internal/tui/components/logo/logo.go+1−1 internal/tui/page/chat/chat.go+381−263 internal/tui/tui.go+1−1
internal/tui/components/chat/chat.go+20−10
@@ -8,14 +8,15 @@ import (
88 tea "github.com/charmbracelet/bubbletea/v2"
99 "github.com/charmbracelet/crush/internal/app"
1010 "github.com/charmbracelet/crush/internal/llm/agent"
11+ "github.com/charmbracelet/crush/internal/logging"
1112 "github.com/charmbracelet/crush/internal/message"
1213 "github.com/charmbracelet/crush/internal/pubsub"
1314 "github.com/charmbracelet/crush/internal/session"
1415 "github.com/charmbracelet/crush/internal/tui/components/chat/messages"
1516 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
1617 "github.com/charmbracelet/crush/internal/tui/components/core/list"
18+ "github.com/charmbracelet/crush/internal/tui/styles"
1719 "github.com/charmbracelet/crush/internal/tui/util"
18− "github.com/charmbracelet/lipgloss/v2"
1920)
2021
2122type SendMsg struct {
@@ -37,6 +38,9 @@ type MessageListCmp interface {
3738 util.Model
3839 layout.Sizeable
3940 layout.Focusable
41+ layout.Help
42+
43+ SetSession(session.Session) tea.Cmd
4044}
4145
4246// messageListCmp implements MessageListCmp, providing a virtualized list
@@ -53,9 +57,9 @@ type messageListCmp struct {
5357 defaultListKeyMap list.KeyMap
5458}
5559
56−// NewMessagesListCmp creates a new message list component with custom keybindings
60+// New creates a new message list component with custom keybindings
5761// and reverse ordering (newest messages at bottom).
58−func NewMessagesListCmp(app *app.App) MessageListCmp {
62+func New(app *app.App) MessageListCmp {
5963 defaultListKeyMap := list.DefaultKeyMap()
6064 listCmp := list.New(
6165 list.WithGapSize(1),
@@ -70,13 +74,14 @@ func NewMessagesListCmp(app *app.App) MessageListCmp {
7074 }
7175}
7276
73−// Init initializes the component (no initialization needed).
77+// Init initializes the component.
7478func (m *messageListCmp) Init() tea.Cmd {
7579 return tea.Sequence(m.listCmp.Init(), m.listCmp.Blur())
7680}
7781
7882// Update handles incoming messages and updates the component state.
7983func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
84+ logging.Info("messageListCmp.Update", "msg", msg)
8085 switch msg := msg.(type) {
8186 case SessionSelectedMsg:
8287 if msg.ID != m.session.ID {
@@ -102,11 +107,15 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
102107
103108// View renders the message list or an initial screen if empty.
104109func (m *messageListCmp) View() tea.View {
110+ t := styles.CurrentTheme()
105111 return tea.NewView(
106− lipgloss.JoinVertical(
107− lipgloss.Left,
108− m.listCmp.View().String(),
109− ),
112+ t.S().Base.
113+ Padding(1).
114+ Width(m.width).
115+ Height(m.height).
116+ Render(
117+ m.listCmp.View().String(),
118+ ),
110119 )
111120}
112121
@@ -371,6 +380,7 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd
371380
372381// SetSession loads and displays messages for a new session.
373382func (m *messageListCmp) SetSession(session session.Session) tea.Cmd {
383+ logging.Info("messageListCmp.SetSession", "sessionID", session.ID)
374384 if m.session.ID == session.ID {
375385 return nil
376386 }
@@ -489,8 +499,8 @@ func (m *messageListCmp) GetSize() (int, int) {
489499// SetSize updates the component dimensions and propagates to the list component.
490500func (m *messageListCmp) SetSize(width int, height int) tea.Cmd {
491501 m.width = width
492− m.height = height - 1
493− return m.listCmp.SetSize(width, height-1)
502+ m.height = height
503+ return m.listCmp.SetSize(width-2, height-2) // for padding
494504}
495505
496506// Blur implements MessageListCmp.
internal/tui/components/chat/editor/editor.go+20−6
@@ -18,6 +18,7 @@ import (
1818 "github.com/charmbracelet/crush/internal/session"
1919 "github.com/charmbracelet/crush/internal/tui/components/chat"
2020 "github.com/charmbracelet/crush/internal/tui/components/completions"
21+ "github.com/charmbracelet/crush/internal/tui/components/core/layout"
2122 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
2223 "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
2324 "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
@@ -26,6 +27,16 @@ import (
2627 "github.com/charmbracelet/lipgloss/v2"
2728)
2829
30+type Editor interface {
31+ util.Model
32+ layout.Sizeable
33+ layout.Focusable
34+ layout.Help
35+ layout.Positional
36+
37+ SetSession(session session.Session) tea.Cmd
38+}
39+
2940type FileCompletionItem struct {
3041 Path string // The file path
3142}
@@ -145,11 +156,6 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145156 var cmd tea.Cmd
146157 var cmds []tea.Cmd
147158 switch msg := msg.(type) {
148− case chat.SessionSelectedMsg:
149− if msg.ID != m.session.ID {
150− m.session = msg
151− }
152− return m, nil
153159 case filepicker.FilePickedMsg:
154160 if len(m.attachments) >= maxAttachments {
155161 return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
@@ -372,7 +378,14 @@ func (c *editorCmp) Bindings() []key.Binding {
372378 return c.keyMap.KeyBindings()
373379}
374380
375−func NewEditorCmp(app *app.App) util.Model {
381+// TODO: most likely we do not need to have the session here
382+// we need to move some functionality to the page level
383+func (c *editorCmp) SetSession(session session.Session) tea.Cmd {
384+ c.session = session
385+ return nil
386+}
387+
388+func New(app *app.App) Editor {
376389 t := styles.CurrentTheme()
377390 ta := textarea.New()
378391 ta.SetStyles(t.S().TextArea)
@@ -393,6 +406,7 @@ func NewEditorCmp(app *app.App) util.Model {
393406 ta.Focus()
394407
395408 return &editorCmp{
409+ // TODO: remove the app instance from here
396410 app: app,
397411 textarea: ta,
398412 keyMap: DefaultEditorKeyMap(),
internal/tui/components/chat/header/header.go+10−7
@@ -11,7 +11,6 @@ import (
1111 "github.com/charmbracelet/crush/internal/lsp/protocol"
1212 "github.com/charmbracelet/crush/internal/pubsub"
1313 "github.com/charmbracelet/crush/internal/session"
14− "github.com/charmbracelet/crush/internal/tui/components/chat"
1514 "github.com/charmbracelet/crush/internal/tui/styles"
1615 "github.com/charmbracelet/crush/internal/tui/util"
1716 "github.com/charmbracelet/lipgloss/v2"
@@ -19,7 +18,8 @@ import (
1918
2019type Header interface {
2120 util.Model
22− SetSession(session session.Session)
21+ SetSession(session session.Session) tea.Cmd
22+ SetWidth(width int) tea.Cmd
2323 SetDetailsOpen(open bool)
2424}
2525
@@ -43,10 +43,6 @@ func (h *header) Init() tea.Cmd {
4343
4444func (p *header) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
4545 switch msg := msg.(type) {
46− case tea.WindowSizeMsg:
47− p.width = msg.Width - 2
48− case chat.SessionSelectedMsg:
49− p.session = msg
5046 case pubsub.Event[session.Session]:
5147 if msg.Type == pubsub.UpdatedEvent {
5248 if p.session.ID == msg.Payload.ID {
@@ -131,6 +127,13 @@ func (h *header) SetDetailsOpen(open bool) {
131127}
132128
133129// SetSession implements Header.
134−func (h *header) SetSession(session session.Session) {
130+func (h *header) SetSession(session session.Session) tea.Cmd {
135131 h.session = session
132+ return nil
133+}
134+
135+// SetWidth implements Header.
136+func (h *header) SetWidth(width int) tea.Cmd {
137+ h.width = width
138+ return nil
136139}
internal/tui/components/chat/sidebar/sidebar.go+34−29
@@ -13,7 +13,6 @@ import (
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/fsext"
1515 "github.com/charmbracelet/crush/internal/history"
16−
1716 "github.com/charmbracelet/crush/internal/lsp"
1817 "github.com/charmbracelet/crush/internal/lsp/protocol"
1918 "github.com/charmbracelet/crush/internal/pubsub"
@@ -29,10 +28,6 @@ import (
2928 "github.com/charmbracelet/x/ansi"
3029)
3130
32−const (
33− logoBreakpoint = 65
34−)
35−
3631type FileHistory struct {
3732 initialVersion history.File
3833 latestVersion history.File
@@ -52,6 +47,7 @@ type Sidebar interface {
5247 util.Model
5348 layout.Sizeable
5449 SetSession(session session.Session) tea.Cmd
50+ SetCompactMode(bool)
5551}
5652
5753type sidebarCmp struct {
@@ -66,7 +62,7 @@ type sidebarCmp struct {
6662 files sync.Map
6763}
6864
69−func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client, compact bool) Sidebar {
65+func New(history history.Service, lspClients map[string]*lsp.Client, compact bool) Sidebar {
7066 return &sidebarCmp{
7167 lspClients: lspClients,
7268 history: history,
@@ -75,15 +71,11 @@ func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client, c
7571}
7672
7773func (m *sidebarCmp) Init() tea.Cmd {
78− m.logo = m.logoBlock(false)
79− m.cwd = cwd()
8074 return nil
8175}
8276
8377func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8478 switch msg := msg.(type) {
85− case chat.SessionSelectedMsg:
86− return m, m.SetSession(msg)
8779 case SessionFilesMsg:
8880 m.files = sync.Map{}
8981 for _, file := range msg.Files {
@@ -137,8 +129,19 @@ func (m *sidebarCmp) View() tea.View {
137129 m.mcpBlock(),
138130 )
139131
132+ // TODO: CHECK out why we need to set the background here weird issue
133+ style := t.S().Base.
134+ Background(t.BgBase).
135+ Width(m.width).
136+ Height(m.height).
137+ Padding(1)
138+ if m.compactMode {
139+ style = style.PaddingTop(0)
140+ }
140141 return tea.NewView(
141− lipgloss.JoinVertical(lipgloss.Left, parts...),
142+ style.Render(
143+ lipgloss.JoinVertical(lipgloss.Left, parts...),
144+ ),
142145 )
143146}
144147
@@ -232,12 +235,8 @@ func (m *sidebarCmp) loadSessionFiles() tea.Msg {
232235}
233236
234237func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
235− if width < logoBreakpoint && (m.width == 0 || m.width >= logoBreakpoint) {
236− m.logo = m.logoBlock(true)
237− } else if width >= logoBreakpoint && (m.width == 0 || m.width < logoBreakpoint) {
238− m.logo = m.logoBlock(false)
239− }
240−
238+ m.logo = m.logoBlock()
239+ m.cwd = cwd()
241240 m.width = width
242241 m.height = height
243242 return nil
@@ -247,9 +246,9 @@ func (m *sidebarCmp) GetSize() (int, int) {
247246 return m.width, m.height
248247}
249248
250−func (m *sidebarCmp) logoBlock(compact bool) string {
249+func (m *sidebarCmp) logoBlock() string {
251250 t := styles.CurrentTheme()
252− return logo.Render(version.Version, compact, logo.Opts{
251+ return logo.Render(version.Version, true, logo.Opts{
253252 FieldColor: t.Primary,
254253 TitleColorA: t.Secondary,
255254 TitleColorB: t.Primary,
@@ -258,12 +257,15 @@ func (m *sidebarCmp) logoBlock(compact bool) string {
258257 })
259258}
260259
260+func (m *sidebarCmp) getMaxWidth() int {
261+ return min(m.width-2, 58) // -2 for padding
262+}
263+
261264func (m *sidebarCmp) filesBlock() string {
262− maxWidth := min(m.width, 58)
263265 t := styles.CurrentTheme()
264266
265267 section := t.S().Subtle.Render(
266− core.Section("Modified Files", maxWidth),
268+ core.Section("Modified Files", m.getMaxWidth()),
267269 )
268270
269271 files := make([]SessionFile, 0)
@@ -304,7 +306,7 @@ func (m *sidebarCmp) filesBlock() string {
304306 filePath := file.FilePath
305307 filePath = strings.TrimPrefix(filePath, cwd)
306308 filePath = fsext.DirTrim(fsext.PrettyPath(filePath), 2)
307− filePath = ansi.Truncate(filePath, maxWidth-lipgloss.Width(extraContent)-2, "…")
309+ filePath = ansi.Truncate(filePath, m.getMaxWidth()-lipgloss.Width(extraContent)-2, "…")
308310 fileList = append(fileList,
309311 core.Status(
310312 core.StatusOpts{
@@ -313,7 +315,7 @@ func (m *sidebarCmp) filesBlock() string {
313315 Title: filePath,
314316 ExtraContent: extraContent,
315317 },
316− m.width,
318+ m.getMaxWidth(),
317319 ),
318320 )
319321 }
@@ -325,11 +327,10 @@ func (m *sidebarCmp) filesBlock() string {
325327}
326328
327329func (m *sidebarCmp) lspBlock() string {
328− maxWidth := min(m.width, 58)
329330 t := styles.CurrentTheme()
330331
331332 section := t.S().Subtle.Render(
332− core.Section("LSPs", maxWidth),
333+ core.Section("LSPs", m.getMaxWidth()),
333334 )
334335
335336 lspList := []string{section, ""}
@@ -387,7 +388,7 @@ func (m *sidebarCmp) lspBlock() string {
387388 Description: l.Command,
388389 ExtraContent: strings.Join(errs, " "),
389390 },
390− m.width,
391+ m.getMaxWidth(),
391392 ),
392393 )
393394 }
@@ -399,11 +400,10 @@ func (m *sidebarCmp) lspBlock() string {
399400}
400401
401402func (m *sidebarCmp) mcpBlock() string {
402− maxWidth := min(m.width, 58)
403403 t := styles.CurrentTheme()
404404
405405 section := t.S().Subtle.Render(
406− core.Section("MCPs", maxWidth),
406+ core.Section("MCPs", m.getMaxWidth()),
407407 )
408408
409409 mcpList := []string{section, ""}
@@ -427,7 +427,7 @@ func (m *sidebarCmp) mcpBlock() string {
427427 Title: n,
428428 Description: l.Command,
429429 },
430− m.width,
430+ m.getMaxWidth(),
431431 ),
432432 )
433433 }
@@ -510,6 +510,11 @@ func (m *sidebarCmp) SetSession(session session.Session) tea.Cmd {
510510 return m.loadSessionFiles
511511}
512512
513+// SetCompactMode sets the compact mode for the sidebar.
514+func (m *sidebarCmp) SetCompactMode(compact bool) {
515+ m.compactMode = compact
516+}
517+
513518func cwd() string {
514519 cwd := config.Get().WorkingDir()
515520 t := styles.CurrentTheme()
internal/tui/components/chat/splash/splash.go+19−2
@@ -17,6 +17,11 @@ type Splash interface {
1717 layout.Help
1818}
1919
20+const (
21+ SplashScreenPaddingX = 2 // Padding X for the splash screen
22+ SplashScreenPaddingY = 1 // Padding Y for the splash screen
23+)
24+
2025type splashCmp struct {
2126 width, height int
2227 keyMap KeyMap
@@ -61,8 +66,20 @@ func (s *splashCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6166
6267// View implements SplashPage.
6368func (s *splashCmp) View() tea.View {
69+ t := styles.CurrentTheme()
6470 content := lipgloss.JoinVertical(lipgloss.Left, s.logoRendered)
65− return tea.NewView(content)
71+ return tea.NewView(
72+ t.S().Base.
73+ Width(s.width).
74+ Height(s.height).
75+ PaddingTop(SplashScreenPaddingY).
76+ PaddingLeft(SplashScreenPaddingX).
77+ PaddingRight(SplashScreenPaddingX).
78+ PaddingBottom(SplashScreenPaddingY).
79+ Render(
80+ content,
81+ ),
82+ )
6683}
6784
6885func (s *splashCmp) logoBlock() string {
@@ -74,7 +91,7 @@ func (s *splashCmp) logoBlock() string {
7491 TitleColorB: t.Primary,
7592 CharmColor: t.Secondary,
7693 VersionColor: t.Primary,
77− Width: s.width - padding,
94+ Width: s.width - (SplashScreenPaddingX * 2),
7895 })
7996}
8097
internal/tui/components/core/layout/split.go−5
@@ -1,11 +1,8 @@
11package layout
22
33import (
4− "log/slog"
5−
64 "github.com/charmbracelet/bubbles/v2/key"
75 tea "github.com/charmbracelet/bubbletea/v2"
8−
96 "github.com/charmbracelet/crush/internal/tui/styles"
107 "github.com/charmbracelet/crush/internal/tui/util"
118 "github.com/charmbracelet/lipgloss/v2"
@@ -156,8 +153,6 @@ func (s *splitPaneLayout) View() tea.View {
156153func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
157154 s.width = width
158155 s.height = height
159− slog.Info("Setting split pane size", "width", width, "height", height)
160−
161156 var topHeight, bottomHeight int
162157 var cmds []tea.Cmd
163158 if s.bottomPanel != nil {
internal/tui/components/logo/logo.go+1−1
@@ -85,7 +85,7 @@ func Render(version string, compact bool, o Opts) string {
8585 }
8686
8787 // Right field.
88− rightWidth := max(15, o.Width-crushWidth-leftWidth) // 2 for the gap.
88+ rightWidth := max(15, o.Width-crushWidth-leftWidth-2) // 2 for the gap.
8989 const stepDownAt = 0
9090 rightField := new(strings.Builder)
9191 for i := range fieldHeight {
internal/tui/page/chat/chat.go+381−263
@@ -2,21 +2,27 @@ package chat
22
33import (
44 "context"
5− "strings"
65 "time"
76
87 "github.com/charmbracelet/bubbles/v2/key"
8+ "github.com/charmbracelet/bubbles/v2/spinner"
99 tea "github.com/charmbracelet/bubbletea/v2"
1010 "github.com/charmbracelet/crush/internal/app"
1111 "github.com/charmbracelet/crush/internal/config"
12+ "github.com/charmbracelet/crush/internal/history"
1213 "github.com/charmbracelet/crush/internal/message"
14+ "github.com/charmbracelet/crush/internal/pubsub"
1315 "github.com/charmbracelet/crush/internal/session"
16+ "github.com/charmbracelet/crush/internal/tui/components/anim"
1417 "github.com/charmbracelet/crush/internal/tui/components/chat"
1518 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
1619 "github.com/charmbracelet/crush/internal/tui/components/chat/header"
1720 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
21+ "github.com/charmbracelet/crush/internal/tui/components/chat/splash"
22+ "github.com/charmbracelet/crush/internal/tui/components/completions"
1823 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
1924 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
25+ "github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
2026 "github.com/charmbracelet/crush/internal/tui/page"
2127 "github.com/charmbracelet/crush/internal/tui/styles"
2228 "github.com/charmbracelet/crush/internal/tui/util"
@@ -26,8 +32,6 @@ import (
2632
2733var ChatPageID page.PageID = "chat"
2834
29−const CompactModeBreakpoint = 120 // Width at which the chat page switches to compact mode
30−
3135type (
3236 OpenFilePickerMsg struct{}
3337 ChatFocusedMsg struct {
@@ -36,104 +40,171 @@ type (
3640 CancelTimerExpiredMsg struct{}
3741)
3842
43+type ChatState string
44+
45+const (
46+ ChatStateOnboarding ChatState = "onboarding"
47+ ChatStateInitProject ChatState = "init_project"
48+ ChatStateNewMessage ChatState = "new_message"
49+ ChatStateInSession ChatState = "in_session"
50+)
51+
52+type PanelType string
53+
54+const (
55+ PanelTypeChat PanelType = "chat"
56+ PanelTypeEditor PanelType = "editor"
57+ PanelTypeSplash PanelType = "splash"
58+)
59+
60+const (
61+ CompactModeBreakpoint = 120 // Width at which the chat page switches to compact mode
62+ EditorHeight = 5 // Height of the editor input area including padding
63+ SideBarWidth = 31 // Width of the sidebar
64+ SideBarDetailsPadding = 1 // Padding for the sidebar details section
65+ HeaderHeight = 1 // Height of the header
66+)
67+
3968type ChatPage interface {
4069 util.Model
4170 layout.Help
4271}
4372
44−type chatPage struct {
45− wWidth, wHeight int // Window dimensions
46− app *app.App
47−
48− layout layout.SplitPaneLayout
49−
50− session session.Session
73+// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
74+func cancelTimerCmd() tea.Cmd {
75+ return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
76+ return CancelTimerExpiredMsg{}
77+ })
78+}
5179
52− keyMap KeyMap
80+type chatPage struct {
81+ width, height int
82+ detailsWidth, detailsHeight int
83+ app *app.App
84+ state ChatState
85+ session session.Session
86+ keyMap KeyMap
87+ focusedPane PanelType
88+ // Compact mode
89+ compact bool
90+ header header.Header
91+ showingDetails bool
92+
93+ sidebar sidebar.Sidebar
94+ chat chat.MessageListCmp
95+ editor editor.Editor
96+ splash splash.Splash
97+ canceling bool
98+
99+ // This will force the compact mode even in big screens
100+ // usually triggered by the user command
101+ // this will also be set when the user config is set to compact mode
102+ forceCompact bool
103+}
53104
54− chatFocused bool
105+func New(app *app.App) ChatPage {
106+ return &chatPage{
107+ app: app,
108+ state: ChatStateOnboarding,
55109
56− compactMode bool
57− forceCompactMode bool // Force compact mode regardless of window size
58− showDetails bool // Show details in the header
59− header header.Header
60− compactSidebar layout.Container
110+ keyMap: DefaultKeyMap(),
61111
62− cancelPending bool // True if ESC was pressed once and waiting for second press
112+ header: header.New(app.LSPClients),
113+ sidebar: sidebar.New(app.History, app.LSPClients, false),
114+ chat: chat.New(app),
115+ editor: editor.New(app),
116+ splash: splash.New(),
117+ }
63118}
64119
65120func (p *chatPage) Init() tea.Cmd {
121+ cfg := config.Get()
122+ if cfg.IsReady() {
123+ if b, _ := config.ProjectNeedsInitialization(); b {
124+ p.state = ChatStateInitProject
125+ } else {
126+ p.state = ChatStateNewMessage
127+ p.focusedPane = PanelTypeEditor
128+ }
129+ }
130+
131+ compact := cfg.Options.TUI.CompactMode
132+ p.compact = compact
133+ p.forceCompact = compact
134+ p.sidebar.SetCompactMode(p.compact)
66135 return tea.Batch(
67− p.layout.Init(),
68− p.compactSidebar.Init(),
69− p.layout.FocusPanel(layout.BottomPanel), // Focus on the bottom panel (editor),
136+ p.header.Init(),
137+ p.sidebar.Init(),
138+ p.chat.Init(),
139+ p.editor.Init(),
140+ p.splash.Init(),
70141 )
71142}
72143
73−// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds
74−func (p *chatPage) cancelTimerCmd() tea.Cmd {
75− return tea.Tick(2*time.Second, func(time.Time) tea.Msg {
76− return CancelTimerExpiredMsg{}
77− })
78−}
79−
80144func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
81145 var cmds []tea.Cmd
82146 switch msg := msg.(type) {
147+ case tea.WindowSizeMsg:
148+ return p, p.SetSize(msg.Width, msg.Height)
83149 case CancelTimerExpiredMsg:
84− p.cancelPending = false
150+ p.canceling = false
85151 return p, nil
86− case tea.WindowSizeMsg:
87− h, cmd := p.header.Update(msg)
88− cmds = append(cmds, cmd)
89− p.header = h.(header.Header)
90− cmds = append(cmds, p.compactSidebar.SetSize(msg.Width-4, 0))
91− // the mode is only relevant when there is a session
92− if p.session.ID != "" {
93− // Only auto-switch to compact mode if not forced
94− if !p.forceCompactMode {
95− if msg.Width <= CompactModeBreakpoint && p.wWidth > CompactModeBreakpoint {
96− p.wWidth = msg.Width
97− p.wHeight = msg.Height
98− cmds = append(cmds, p.setCompactMode(true))
99− return p, tea.Batch(cmds...)
100− } else if msg.Width > CompactModeBreakpoint && p.wWidth <= CompactModeBreakpoint {
101− p.wWidth = msg.Width
102− p.wHeight = msg.Height
103− return p, p.setCompactMode(false)
104− }
105− }
106− }
107− p.wWidth = msg.Width
108− p.wHeight = msg.Height
109− layoutHeight := msg.Height
110− if p.compactMode {
111− // make space for the header
112− layoutHeight -= 1
152+ case chat.SendMsg:
153+ return p, p.sendMessage(msg.Text, msg.Attachments)
154+ case chat.SessionSelectedMsg:
155+ return p, p.setSession(msg)
156+ case commands.ToggleCompactModeMsg:
157+ p.forceCompact = !p.forceCompact
158+ if p.forceCompact {
159+ p.setCompactMode(true)
160+ } else if p.width >= CompactModeBreakpoint {
161+ p.setCompactMode(false)
113162 }
114− cmd = p.layout.SetSize(msg.Width, layoutHeight)
163+ return p, p.SetSize(p.width, p.height)
164+ case pubsub.Event[session.Session]:
165+ // this needs to go to header/sidebar
166+ u, cmd := p.header.Update(msg)
167+ p.header = u.(header.Header)
168+ cmds = append(cmds, cmd)
169+ u, cmd = p.sidebar.Update(msg)
170+ p.sidebar = u.(sidebar.Sidebar)
171+ cmds = append(cmds, cmd)
172+ return p, tea.Batch(cmds...)
173+ case chat.SessionClearedMsg:
174+ u, cmd := p.header.Update(msg)
175+ p.header = u.(header.Header)
176+ cmds = append(cmds, cmd)
177+ u, cmd = p.sidebar.Update(msg)
178+ p.sidebar = u.(sidebar.Sidebar)
179+ cmds = append(cmds, cmd)
180+ u, cmd = p.chat.Update(msg)
181+ p.chat = u.(chat.MessageListCmp)
182+ cmds = append(cmds, cmd)
183+ return p, tea.Batch(cmds...)
184+ case filepicker.FilePickedMsg,
185+ completions.CompletionsClosedMsg,
186+ completions.SelectCompletionMsg:
187+ u, cmd := p.editor.Update(msg)
188+ p.editor = u.(editor.Editor)
189+ cmds = append(cmds, cmd)
190+ return p, tea.Batch(cmds...)
191+
192+ case pubsub.Event[message.Message],
193+ anim.StepMsg,
194+ spinner.TickMsg:
195+ // this needs to go to chat
196+ u, cmd := p.chat.Update(msg)
197+ p.chat = u.(chat.MessageListCmp)
198+ cmds = append(cmds, cmd)
199+ return p, tea.Batch(cmds...)
200+
201+ case pubsub.Event[history.File], sidebar.SessionFilesMsg:
202+ // this needs to go to sidebar
203+ u, cmd := p.sidebar.Update(msg)
204+ p.sidebar = u.(sidebar.Sidebar)
115205 cmds = append(cmds, cmd)
116206 return p, tea.Batch(cmds...)
117207
118− case chat.SendMsg:
119− cmd := p.sendMessage(msg.Text, msg.Attachments)
120− if cmd != nil {
121− return p, cmd
122− }
123− case commands.ToggleCompactModeMsg:
124− // Only allow toggling if window width is larger than compact breakpoint
125− if p.wWidth > CompactModeBreakpoint {
126− p.forceCompactMode = !p.forceCompactMode
127− // If force compact mode is enabled, switch to compact mode
128− // If force compact mode is disabled, switch based on window size
129− if p.forceCompactMode {
130− return p, p.setCompactMode(true)
131− } else {
132− // Return to auto mode based on window size
133− shouldBeCompact := p.wWidth <= CompactModeBreakpoint
134− return p, p.setCompactMode(shouldBeCompact)
135− }
136− }
137208 case commands.CommandRunCustomMsg:
138209 // Check if the agent is busy before executing custom commands
139210 if p.app.CoderAgent.IsBusy() {
@@ -145,30 +216,10 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
145216 if cmd != nil {
146217 return p, cmd
147218 }
148− case chat.SessionSelectedMsg:
149− if p.session.ID == "" {
150− cmd := p.setMessages()
151− if cmd != nil {
152− cmds = append(cmds, cmd)
153− }
154− }
155− needsModeChange := p.session.ID == ""
156− p.session = msg
157− p.header.SetSession(msg)
158− if needsModeChange && (p.wWidth <= CompactModeBreakpoint || p.forceCompactMode) {
159− cmds = append(cmds, p.setCompactMode(true))
160− }
161219 case tea.KeyPressMsg:
162220 switch {
163221 case key.Matches(msg, p.keyMap.NewSession):
164− p.session = session.Session{}
165− return p, tea.Batch(
166− p.clearMessages(),
167− util.CmdHandler(chat.SessionClearedMsg{}),
168− p.setCompactMode(false),
169− p.layout.FocusPanel(layout.BottomPanel),
170− util.CmdHandler(ChatFocusedMsg{Focused: false}),
171− )
222+ return p, p.newSession()
172223 case key.Matches(msg, p.keyMap.AddAttachment):
173224 agentCfg := config.Get().Agents["coder"]
174225 model := config.Get().GetModelByType(agentCfg.Model)
@@ -178,167 +229,264 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
178229 return p, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
179230 }
180231 case key.Matches(msg, p.keyMap.Tab):
181− if p.session.ID == "" {
182− return p, nil
183− }
184− p.chatFocused = !p.chatFocused
185− if p.chatFocused {
186− cmds = append(cmds, p.layout.FocusPanel(layout.LeftPanel))
187− cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: true}))
188− } else {
189− cmds = append(cmds, p.layout.FocusPanel(layout.BottomPanel))
190− cmds = append(cmds, util.CmdHandler(ChatFocusedMsg{Focused: false}))
191− }
192− return p, tea.Batch(cmds...)
232+ p.changeFocus()
233+ return p, nil
193234 case key.Matches(msg, p.keyMap.Cancel):
194− if p.session.ID != "" {
195− if p.cancelPending {
196− // Second ESC press - actually cancel the session
197− p.cancelPending = false
198− p.app.CoderAgent.Cancel(p.session.ID)
199− return p, nil
200− } else {
201− // First ESC press - start the timer
202− p.cancelPending = true
203− return p, p.cancelTimerCmd()
204− }
205− }
235+ return p, p.cancel()
206236 case key.Matches(msg, p.keyMap.Details):
207− if p.session.ID == "" || !p.compactMode {
208− return p, nil // No session to show details for
209− }
210− p.showDetails = !p.showDetails
211− p.header.SetDetailsOpen(p.showDetails)
212− if p.showDetails {
213− return p, tea.Batch()
214− }
215−
237+ p.showDetails()
216238 return p, nil
217239 }
240+
241+ // Send the key press to the focused pane
242+ switch p.focusedPane {
243+ case PanelTypeChat:
244+ u, cmd := p.chat.Update(msg)
245+ p.chat = u.(chat.MessageListCmp)
246+ cmds = append(cmds, cmd)
247+ case PanelTypeEditor:
248+ u, cmd := p.editor.Update(msg)
249+ p.editor = u.(editor.Editor)
250+ cmds = append(cmds, cmd)
251+ }
218252 }
219− u, cmd := p.layout.Update(msg)
220− cmds = append(cmds, cmd)
221− p.layout = u.(layout.SplitPaneLayout)
222− h, cmd := p.header.Update(msg)
223− p.header = h.(header.Header)
224− cmds = append(cmds, cmd)
225− s, cmd := p.compactSidebar.Update(msg)
226− p.compactSidebar = s.(layout.Container)
227− cmds = append(cmds, cmd)
228253 return p, tea.Batch(cmds...)
229254}
230255
231−func (p *chatPage) setMessages() tea.Cmd {
232− messagesContainer := layout.NewContainer(
233− chat.NewMessagesListCmp(p.app),
234− layout.WithPadding(1, 1, 0, 1),
235− )
236− return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
237−}
256+func (p *chatPage) View() tea.View {
257+ var chatView tea.View
258+ t := styles.CurrentTheme()
259+ switch p.state {
260+ case ChatStateOnboarding, ChatStateInitProject:
261+ chatView = tea.NewView(
262+ t.S().Base.Render(
263+ p.splash.View().String(),
264+ ),
265+ )
266+ case ChatStateNewMessage:
267+ editorView := p.editor.View()
268+ chatView = tea.NewView(
269+ lipgloss.JoinVertical(
270+ lipgloss.Left,
271+ t.S().Base.Render(
272+ p.splash.View().String(),
273+ ),
274+ editorView.String(),
275+ ),
276+ )
277+ chatView.SetCursor(editorView.Cursor())
278+ case ChatStateInSession:
279+ messagesView := p.chat.View()
280+ editorView := p.editor.View()
281+ if p.compact {
282+ headerView := p.header.View()
283+ chatView = tea.NewView(
284+ lipgloss.JoinVertical(
285+ lipgloss.Left,
286+ headerView.String(),
287+ messagesView.String(),
288+ editorView.String(),
289+ ),
290+ )
291+ chatView.SetCursor(editorView.Cursor())
292+ } else {
293+ sidebarView := p.sidebar.View()
294+ messages := lipgloss.JoinHorizontal(
295+ lipgloss.Left,
296+ messagesView.String(),
297+ sidebarView.String(),
298+ )
299+ chatView = tea.NewView(
300+ lipgloss.JoinVertical(
301+ lipgloss.Left,
302+ messages,
303+ p.editor.View().String(),
304+ ),
305+ )
306+ chatView.SetCursor(editorView.Cursor())
307+ }
308+ default:
309+ chatView = tea.NewView("Unknown chat state")
310+ }
238311
239−func (p *chatPage) setSidebar() tea.Cmd {
240− sidebarContainer := sidebarCmp(p.app, false, p.session)
241− sidebarContainer.Init()
242− return p.layout.SetRightPanel(sidebarContainer)
243−}
312+ layers := []*lipgloss.Layer{
313+ lipgloss.NewLayer(chatView.String()).X(0).Y(0),
314+ }
244315
245−func (p *chatPage) clearMessages() tea.Cmd {
246− return p.layout.ClearLeftPanel()
316+ if p.showingDetails {
317+ style := t.S().Base.
318+ Width(p.detailsWidth).
319+ Border(lipgloss.RoundedBorder()).
320+ BorderForeground(t.BorderFocus)
321+ version := t.S().Subtle.Width(p.detailsWidth - 2).AlignHorizontal(lipgloss.Right).Render(version.Version)
322+ details := style.Render(
323+ lipgloss.JoinVertical(
324+ lipgloss.Left,
325+ p.sidebar.View().String(),
326+ version,
327+ ),
328+ )
329+ layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
330+ }
331+ canvas := lipgloss.NewCanvas(
332+ layers...,
333+ )
334+ view := tea.NewView(canvas.Render())
335+ view.SetCursor(chatView.Cursor())
336+ return view
247337}
248338
249−func (p *chatPage) setCompactMode(compact bool) tea.Cmd {
250− p.compactMode = compact
251− var cmds []tea.Cmd
339+func (p *chatPage) setCompactMode(compact bool) {
340+ if p.compact == compact {
341+ return
342+ }
343+ p.compact = compact
252344 if compact {
253− // add offset for the header
254− p.layout.SetOffset(0, 1)
255− // make space for the header
256− cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight-1))
257− // remove the sidebar
258− cmds = append(cmds, p.layout.ClearRightPanel())
259− return tea.Batch(cmds...)
345+ p.compact = true
346+ p.sidebar.SetCompactMode(true)
260347 } else {
261− // remove the offset for the header
262− p.layout.SetOffset(0, 0)
263− // restore the original size
264− cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight))
265− // set the sidebar
266− cmds = append(cmds, p.setSidebar())
267− l, cmd := p.layout.Update(chat.SessionSelectedMsg(p.session))
268− p.layout = l.(layout.SplitPaneLayout)
269− cmds = append(cmds, cmd)
348+ p.compact = false
349+ p.showingDetails = false
350+ p.sidebar.SetCompactMode(false)
351+ }
352+}
270353
271− return tea.Batch(cmds...)
354+func (p *chatPage) handleCompactMode(newWidth int) {
355+ if p.forceCompact {
356+ return
357+ }
358+ if newWidth < CompactModeBreakpoint && !p.compact {
359+ p.setCompactMode(true)
360+ }
361+ if newWidth >= CompactModeBreakpoint && p.compact {
362+ p.setCompactMode(false)
272363 }
273364}
274365
275−func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
366+func (p *chatPage) SetSize(width, height int) tea.Cmd {
367+ p.handleCompactMode(width)
368+ p.width = width
369+ p.height = height
276370 var cmds []tea.Cmd
277− if p.session.ID == "" {
278− session, err := p.app.Sessions.Create(context.Background(), "New Session")
279− if err != nil {
280− return util.ReportError(err)
371+ switch p.state {
372+ case ChatStateOnboarding, ChatStateInitProject:
373+ // here we should just have the splash screen
374+ cmds = append(cmds, p.splash.SetSize(width, height))
375+ case ChatStateNewMessage:
376+ cmds = append(cmds, p.splash.SetSize(width, height-EditorHeight))
377+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
378+ cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
379+ case ChatStateInSession:
380+ if p.compact {
381+ cmds = append(cmds, p.chat.SetSize(width, height-EditorHeight-HeaderHeight))
382+ // In compact mode, the sidebar is shown in the details section, the width needs to be adjusted for the padding and border
383+ p.detailsWidth = width - 2 // because of position
384+ cmds = append(cmds, p.sidebar.SetSize(p.detailsWidth-2, p.detailsHeight-2)) // adjust for border
385+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
386+ cmds = append(cmds, p.header.SetWidth(width-1))
387+ } else {
388+ cmds = append(cmds, p.chat.SetSize(width-SideBarWidth, height-EditorHeight))
389+ cmds = append(cmds, p.editor.SetSize(width, EditorHeight))
390+ cmds = append(cmds, p.sidebar.SetSize(SideBarWidth, height-EditorHeight))
281391 }
392+ cmds = append(cmds, p.editor.SetPosition(0, height-EditorHeight))
393+ }
394+ return tea.Batch(cmds...)
395+}
282396
283− p.session = session
284− cmd := p.setMessages()
285− if cmd != nil {
286− cmds = append(cmds, cmd)
287− }
288− cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
397+func (p *chatPage) newSession() tea.Cmd {
398+ if p.state != ChatStateInSession {
399+ // Cannot start a new session if we are not in the session state
400+ return nil
289401 }
402+ // blank session
403+ p.session = session.Session{}
404+ p.state = ChatStateNewMessage
405+ p.focusedPane = PanelTypeEditor
406+ p.canceling = false
407+ // Reset the chat and editor components
408+ return tea.Batch(
409+ util.CmdHandler(chat.SessionClearedMsg{}),
410+ p.SetSize(p.width, p.height),
411+ )
412+}
290413
291− _, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
292− if err != nil {
293− return util.ReportError(err)
414+func (p *chatPage) setSession(session session.Session) tea.Cmd {
415+ if p.session.ID == session.ID {
416+ return nil
294417 }
295− return tea.Batch(cmds...)
418+
419+ var cmds []tea.Cmd
420+ p.session = session
421+ // We want to first resize the components
422+ if p.state != ChatStateInSession {
423+ p.state = ChatStateInSession
424+ cmds = append(cmds, p.SetSize(p.width, p.height))
425+ }
426+ cmds = append(cmds, p.chat.SetSession(session))
427+ cmds = append(cmds, p.sidebar.SetSession(session))
428+ cmds = append(cmds, p.header.SetSession(session))
429+ cmds = append(cmds, p.editor.SetSession(session))
430+
431+ return tea.Sequence(cmds...)
296432}
297433
298−func (p *chatPage) SetSize(width, height int) tea.Cmd {
299− return p.layout.SetSize(width, height)
434+func (p *chatPage) changeFocus() {
435+ if p.state != ChatStateInSession {
436+ // Cannot change focus if we are not in the session state
437+ return
438+ }
439+ switch p.focusedPane {
440+ case PanelTypeChat:
441+ p.focusedPane = PanelTypeEditor
442+ case PanelTypeEditor:
443+ p.focusedPane = PanelTypeChat
444+ }
300445}
301446
302−func (p *chatPage) GetSize() (int, int) {
303− return p.layout.GetSize()
447+func (p *chatPage) cancel() tea.Cmd {
448+ if p.state != ChatStateInSession || !p.app.CoderAgent.IsBusy() {
449+ // Cannot cancel if we are not in the session state
450+ return nil
451+ }
452+
453+ // second press of cancel key will actually cancel the session
454+ if p.canceling {
455+ p.canceling = false
456+ p.app.CoderAgent.Cancel(p.session.ID)
457+ return nil
458+ }
459+
460+ p.canceling = true
461+ return cancelTimerCmd()
304462}
305463
306−func (p *chatPage) View() tea.View {
307− if !p.compactMode || p.session.ID == "" {
308− // If not in compact mode or there is no session, we don't show the header
309− return p.layout.View()
464+func (p *chatPage) showDetails() {
465+ if p.state != ChatStateInSession || !p.compact {
466+ // Cannot show details if we are not in the session state or if we are not in compact mode
467+ return
310468 }
311− layoutView := p.layout.View()
312− chatView := strings.Join(
313− []string{
314− p.header.View().String(),
315− layoutView.String(),
316− }, "\n",
317− )
318− layers := []*lipgloss.Layer{
319− lipgloss.NewLayer(chatView).X(0).Y(0),
469+ p.showingDetails = !p.showingDetails
470+ p.header.SetDetailsOpen(p.showingDetails)
471+}
472+
473+func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
474+ session := p.session
475+ var cmds []tea.Cmd
476+ if p.state != ChatStateInSession {
477+ // branch new session
478+ newSession, err := p.app.Sessions.Create(context.Background(), "New Session")
479+ if err != nil {
480+ return util.ReportError(err)
481+ }
482+ session = newSession
483+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
320484 }
321− if p.showDetails {
322− t := styles.CurrentTheme()
323− style := t.S().Base.
324− Border(lipgloss.RoundedBorder()).
325− BorderForeground(t.BorderFocus)
326− version := t.S().Subtle.Padding(0, 1).AlignHorizontal(lipgloss.Right).Width(p.wWidth - 4).Render(version.Version)
327− details := style.Render(
328− lipgloss.JoinVertical(
329− lipgloss.Left,
330− p.compactSidebar.View().String(),
331− version,
332− ),
333− )
334− layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1))
485+ _, err := p.app.CoderAgent.Run(context.Background(), session.ID, text, attachments...)
486+ if err != nil {
487+ return util.ReportError(err)
335488 }
336− canvas := lipgloss.NewCanvas(
337− layers...,
338− )
339− view := tea.NewView(canvas.Render())
340− view.SetCursor(layoutView.Cursor())
341− return view
489+ return tea.Batch(cmds...)
342490}
343491
344492func (p *chatPage) Bindings() []key.Binding {
@@ -348,7 +496,7 @@ func (p *chatPage) Bindings() []key.Binding {
348496 }
349497 if p.app.CoderAgent.IsBusy() {
350498 cancelBinding := p.keyMap.Cancel
351− if p.cancelPending {
499+ if p.canceling {
352500 cancelBinding = key.NewBinding(
353501 key.WithKeys("esc"),
354502 key.WithHelp("esc", "press again to cancel"),
@@ -357,56 +505,26 @@ func (p *chatPage) Bindings() []key.Binding {
357505 bindings = append([]key.Binding{cancelBinding}, bindings...)
358506 }
359507
360− if p.chatFocused {
508+ switch p.focusedPane {
509+ case PanelTypeChat:
361510 bindings = append([]key.Binding{
362511 key.NewBinding(
363512 key.WithKeys("tab"),
364513 key.WithHelp("tab", "focus editor"),
365514 ),
366515 }, bindings...)
367− } else {
516+ bindings = append(bindings, p.chat.Bindings()...)
517+ case PanelTypeEditor:
368518 bindings = append([]key.Binding{
369519 key.NewBinding(
370520 key.WithKeys("tab"),
371521 key.WithHelp("tab", "focus chat"),
372522 ),
373523 }, bindings...)
524+ bindings = append(bindings, p.editor.Bindings()...)
525+ case PanelTypeSplash:
526+ bindings = append(bindings, p.splash.Bindings()...)
374527 }
375528
376− bindings = append(bindings, p.layout.Bindings()...)
377529 return bindings
378530}
379−
380−func sidebarCmp(app *app.App, compact bool, session session.Session) layout.Container {
381− padding := layout.WithPadding(1, 1, 1, 1)
382− if compact {
383− padding = layout.WithPadding(0, 1, 1, 1)
384− }
385− sidebar := sidebar.NewSidebarCmp(app.History, app.LSPClients, compact)
386− if session.ID != "" {
387− sidebar.SetSession(session)
388− }
389−
390− return layout.NewContainer(
391− sidebar,
392− padding,
393− )
394−}
395−
396−func NewChatPage(app *app.App) ChatPage {
397− editorContainer := layout.NewContainer(
398− editor.NewEditorCmp(app),
399− )
400− return &chatPage{
401− app: app,
402− layout: layout.NewSplitPane(
403− layout.WithRightPanel(sidebarCmp(app, false, session.Session{})),
404− layout.WithBottomPanel(editorContainer),
405− layout.WithFixedBottomHeight(5),
406− layout.WithFixedRightWidth(31),
407− ),
408− compactSidebar: sidebarCmp(app, true, session.Session{}),
409− keyMap: DefaultKeyMap(),
410− header: header.New(app.LSPClients),
411− }
412−}
internal/tui/tui.go+1−1
@@ -419,7 +419,7 @@ func (a *appModel) View() tea.View {
419419
420420// New creates and initializes a new TUI application model.
421421func New(app *app.App) tea.Model {
422− chatPage := chat.NewChatPage(app)
422+ chatPage := chat.New(app)
423423 keyMap := DefaultKeyMap()
424424 keyMap.pageBindings = chatPage.Bindings()
425425