@@ -2,6 +2,7 @@ package chat
22
33import (
44 "context"
5+ "strings"
56
67 "github.com/charmbracelet/bubbles/v2/key"
78 tea "github.com/charmbracelet/bubbletea/v2"
@@ -12,6 +13,7 @@ import (
1213 "github.com/charmbracelet/crush/internal/session"
1314 "github.com/charmbracelet/crush/internal/tui/components/chat"
1415 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
16+ "github.com/charmbracelet/crush/internal/tui/components/chat/header"
1517 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
1618 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
1719 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
@@ -21,6 +23,8 @@ import (
2123
2224var ChatPageID page.PageID = "chat"
2325
26+const CompactModeBreakpoint = 90 // Width at which the chat page switches to compact mode
27+
2428type (
2529 OpenFilePickerMsg struct{}
2630 ChatFocusedMsg struct {
@@ -34,7 +38,8 @@ type ChatPage interface {
3438}
3539
3640type chatPage struct {
37− app *app.App
41+ wWidth, wHeight int // Window dimensions
42+ app *app.App
3843
3944 layout layout.SplitPaneLayout
4045
@@ -43,6 +48,10 @@ type chatPage struct {
4348 keyMap KeyMap
4449
4550 chatFocused bool
51+
52+ compactMode bool
53+ forceCompactMode bool // Force compact mode regardless of window size
54+ header header.Header
4655}
4756
4857func (p *chatPage) Init() tea.Cmd {
@@ -57,13 +66,55 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5766 var cmds []tea.Cmd
5867 switch msg := msg.(type) {
5968 case tea.WindowSizeMsg:
60− cmd := p.layout.SetSize(msg.Width, msg.Height)
69+ h, cmd := p.header.Update(msg)
6170 cmds = append(cmds, cmd)
71+ p.header = h.(header.Header)
72+ // the mode is only relevant when there is a session
73+ if p.session.ID != "" {
74+ // Only auto-switch to compact mode if not forced
75+ if !p.forceCompactMode {
76+ if msg.Width <= CompactModeBreakpoint && p.wWidth > CompactModeBreakpoint {
77+ p.wWidth = msg.Width
78+ p.wHeight = msg.Height
79+ cmds = append(cmds, p.setCompactMode(true))
80+ return p, tea.Batch(cmds...)
81+ } else if msg.Width > CompactModeBreakpoint && p.wWidth <= CompactModeBreakpoint {
82+ p.wWidth = msg.Width
83+ p.wHeight = msg.Height
84+ return p, p.setCompactMode(false)
85+ }
86+ }
87+ }
88+ p.wWidth = msg.Width
89+ p.wHeight = msg.Height
90+ layoutHeight := msg.Height
91+ if p.compactMode {
92+ // make space for the header
93+ layoutHeight -= 1
94+ }
95+ cmd = p.layout.SetSize(msg.Width, layoutHeight)
96+ cmds = append(cmds, cmd)
97+ return p, tea.Batch(cmds...)
98+
6299 case chat.SendMsg:
63100 cmd := p.sendMessage(msg.Text, msg.Attachments)
64101 if cmd != nil {
65102 return p, cmd
66103 }
104+ case commands.ToggleCompactModeMsg:
105+ // Only allow toggling if window width is larger than compact breakpoint
106+ if p.wWidth > CompactModeBreakpoint {
107+ p.forceCompactMode = !p.forceCompactMode
108+ // If force compact mode is enabled, switch to compact mode
109+ // If force compact mode is disabled, switch based on window size
110+ if p.forceCompactMode {
111+ return p, p.setCompactMode(true)
112+ } else {
113+ // Return to auto mode based on window size
114+ shouldBeCompact := p.wWidth <= CompactModeBreakpoint
115+ return p, p.setCompactMode(shouldBeCompact)
116+ }
117+ }
67118 case commands.CommandRunCustomMsg:
68119 // Check if the agent is busy before executing custom commands
69120 if p.app.CoderAgent.IsBusy() {
@@ -82,7 +133,12 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
82133 cmds = append(cmds, cmd)
83134 }
84135 }
136+ needsModeChange := p.session.ID == ""
85137 p.session = msg
138+ p.header.SetSession(msg)
139+ if needsModeChange && (p.wWidth <= CompactModeBreakpoint || p.forceCompactMode) {
140+ cmds = append(cmds, p.setCompactMode(true))
141+ }
86142 case tea.KeyPressMsg:
87143 switch {
88144 case key.Matches(msg, p.keyMap.NewSession):
@@ -90,8 +146,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
90146 return p, tea.Batch(
91147 p.clearMessages(),
92148 util.CmdHandler(chat.SessionClearedMsg{}),
149+ p.setCompactMode(false),
93150 )
94−
95151 case key.Matches(msg, p.keyMap.AddAttachment):
96152 cfg := config.Get()
97153 agentCfg := cfg.Agents[config.AgentCoder]
@@ -128,6 +184,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
128184 cmds = append(cmds, cmd)
129185 p.layout = u.(layout.SplitPaneLayout)
130186
187+ h, cmd := p.header.Update(msg)
188+ cmds = append(cmds, cmd)
189+ p.header = h.(header.Header)
131190 return p, tea.Batch(cmds...)
132191}
133192
@@ -139,10 +198,38 @@ func (p *chatPage) setMessages() tea.Cmd {
139198 return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
140199}
141200
201+func (p *chatPage) setSidebar() tea.Cmd {
202+ sidebarContainer := sidebarCmp(p.app)
203+ sidebarContainer.Init()
204+ return p.layout.SetRightPanel(sidebarContainer)
205+}
206+
142207func (p *chatPage) clearMessages() tea.Cmd {
143208 return p.layout.ClearLeftPanel()
144209}
145210
211+func (p *chatPage) setCompactMode(compact bool) tea.Cmd {
212+ p.compactMode = compact
213+ var cmds []tea.Cmd
214+ if compact {
215+ // add offset for the header
216+ p.layout.SetOffset(0, 1)
217+ // make space for the header
218+ cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight-1))
219+ // remove the sidebar
220+ cmds = append(cmds, p.layout.ClearRightPanel())
221+ return tea.Batch(cmds...)
222+ } else {
223+ // remove the offset for the header
224+ p.layout.SetOffset(0, 0)
225+ // restore the original size
226+ cmds = append(cmds, p.layout.SetSize(p.wWidth, p.wHeight))
227+ // set the sidebar
228+ cmds = append(cmds, p.setSidebar())
229+ return tea.Batch(cmds...)
230+ }
231+}
232+
146233func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
147234 var cmds []tea.Cmd
148235 if p.session.ID == "" {
@@ -175,7 +262,21 @@ func (p *chatPage) GetSize() (int, int) {
175262}
176263
177264func (p *chatPage) View() tea.View {
178− return p.layout.View()
265+ if !p.compactMode || p.session.ID == "" {
266+ // If not in compact mode or there is no session, we don't show the header
267+ return p.layout.View()
268+ }
269+ layoutView := p.layout.View()
270+ chatView := tea.NewView(
271+ strings.Join(
272+ []string{
273+ p.header.View().String(),
274+ layoutView.String(),
275+ }, "\n",
276+ ),
277+ )
278+ chatView.SetCursor(layoutView.Cursor())
279+ return chatView
179280}
180281
181282func (p *chatPage) Bindings() []key.Binding {
@@ -207,22 +308,26 @@ func (p *chatPage) Bindings() []key.Binding {
207308 return bindings
208309}
209310
210−func NewChatPage(app *app.App) ChatPage {
211− sidebarContainer := layout.NewContainer(
311+func sidebarCmp(app *app.App) layout.Container {
312+ return layout.NewContainer(
212313 sidebar.NewSidebarCmp(app.History, app.LSPClients),
213314 layout.WithPadding(1, 1, 1, 1),
214315 )
316+}
317+
318+func NewChatPage(app *app.App) ChatPage {
215319 editorContainer := layout.NewContainer(
216320 editor.NewEditorCmp(app),
217321 )
218322 return &chatPage{
219323 app: app,
220324 layout: layout.NewSplitPane(
221− layout.WithRightPanel(sidebarContainer),
325+ layout.WithRightPanel(sidebarCmp(app)),
222326 layout.WithBottomPanel(editorContainer),
223327 layout.WithFixedBottomHeight(5),
224328 layout.WithFixedRightWidth(31),
225329 ),
226330 keyMap: DefaultKeyMap(),
331+ header: header.New(app.LSPClients),
227332 }
228333}