@@ -2,6 +2,7 @@ package chat
6 "github.com/charmbracelet/bubbles/v2/key"
7 "github.com/charmbracelet/bubbles/v2/key"
7 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
@@ -12,6 +13,7 @@ import (
12 "github.com/charmbracelet/crush/internal/session"
13 "github.com/charmbracelet/crush/internal/session"
13 "github.com/charmbracelet/crush/internal/tui/components/chat"
14 "github.com/charmbracelet/crush/internal/tui/components/chat"
14 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
15 "github.com/charmbracelet/crush/internal/tui/components/chat/editor"
16+ "github.com/charmbracelet/crush/internal/tui/components/chat/header"
15 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
17 "github.com/charmbracelet/crush/internal/tui/components/chat/sidebar"
16 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
18 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
17 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
19 "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
@@ -21,6 +23,8 @@ import (
22var ChatPageID page.PageID = "chat"
24var ChatPageID page.PageID = "chat"
26+const CompactModeBreakpoint = 90 // Width at which the chat page switches to compact mode
25 OpenFilePickerMsg struct{}
29 OpenFilePickerMsg struct{}
26 ChatFocusedMsg struct {
30 ChatFocusedMsg struct {
@@ -34,7 +38,8 @@ type ChatPage interface {
36type chatPage struct {
40type chatPage struct {
41+ wWidth, wHeight int // Window dimensions
37− app *app.App
42+ app *app.App
39 layout layout.SplitPaneLayout
44 layout layout.SplitPaneLayout
@@ -43,6 +48,10 @@ type chatPage struct {
43 keyMap KeyMap
48 keyMap KeyMap
45 chatFocused bool
50 chatFocused bool
53+ forceCompactMode bool // Force compact mode regardless of window size
48func (p *chatPage) Init() tea.Cmd {
57func (p *chatPage) Init() tea.Cmd {
@@ -57,13 +66,55 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
57 var cmds []tea.Cmd
66 var cmds []tea.Cmd
58 switch msg := msg.(type) {
67 switch msg := msg.(type) {
59 case tea.WindowSizeMsg:
68 case tea.WindowSizeMsg:
60− cmd := p.layout.SetSize(msg.Width, msg.Height)
69+ h, cmd := p.header.Update(msg)
61 cmds = append(cmds, cmd)
70 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 {
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 {
83+ p.wHeight = msg.Height
84+ return p, p.setCompactMode(false)
89+ p.wHeight = msg.Height
90+ layoutHeight := msg.Height
92+ // make space for the header
95+ cmd = p.layout.SetSize(msg.Width, layoutHeight)
96+ cmds = append(cmds, cmd)
97+ return p, tea.Batch(cmds...)
62 case chat.SendMsg:
99 case chat.SendMsg:
63 cmd := p.sendMessage(msg.Text, msg.Attachments)
100 cmd := p.sendMessage(msg.Text, msg.Attachments)
64 if cmd != nil {
101 if cmd != nil {
65 return p, cmd
102 return p, cmd
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)
113+ // Return to auto mode based on window size
114+ shouldBeCompact := p.wWidth <= CompactModeBreakpoint
115+ return p, p.setCompactMode(shouldBeCompact)
67 case commands.CommandRunCustomMsg:
118 case commands.CommandRunCustomMsg:
68 // Check if the agent is busy before executing custom commands
119 // Check if the agent is busy before executing custom commands
69 if p.app.CoderAgent.IsBusy() {
120 if p.app.CoderAgent.IsBusy() {
@@ -82,7 +133,12 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
82 cmds = append(cmds, cmd)
133 cmds = append(cmds, cmd)
136+ needsModeChange := p.session.ID == ""
85 p.session = msg
137 p.session = msg
138+ p.header.SetSession(msg)
139+ if needsModeChange && (p.wWidth <= CompactModeBreakpoint || p.forceCompactMode) {
140+ cmds = append(cmds, p.setCompactMode(true))
86 case tea.KeyPressMsg:
142 case tea.KeyPressMsg:
88 case key.Matches(msg, p.keyMap.NewSession):
144 case key.Matches(msg, p.keyMap.NewSession):
@@ -90,8 +146,8 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
90 return p, tea.Batch(
146 return p, tea.Batch(
91 p.clearMessages(),
147 p.clearMessages(),
92 util.CmdHandler(chat.SessionClearedMsg{}),
148 util.CmdHandler(chat.SessionClearedMsg{}),
149+ p.setCompactMode(false),
95 case key.Matches(msg, p.keyMap.AddAttachment):
151 case key.Matches(msg, p.keyMap.AddAttachment):
96 cfg := config.Get()
152 cfg := config.Get()
97 agentCfg := cfg.Agents[config.AgentCoder]
153 agentCfg := cfg.Agents[config.AgentCoder]
@@ -128,6 +184,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
128 cmds = append(cmds, cmd)
184 cmds = append(cmds, cmd)
129 p.layout = u.(layout.SplitPaneLayout)
185 p.layout = u.(layout.SplitPaneLayout)
187+ h, cmd := p.header.Update(msg)
188+ cmds = append(cmds, cmd)
189+ p.header = h.(header.Header)
131 return p, tea.Batch(cmds...)
190 return p, tea.Batch(cmds...)
@@ -139,10 +198,38 @@ func (p *chatPage) setMessages() tea.Cmd {
139 return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
198 return tea.Batch(p.layout.SetLeftPanel(messagesContainer), messagesContainer.Init())
201+func (p *chatPage) setSidebar() tea.Cmd {
202+ sidebarContainer := sidebarCmp(p.app)
203+ sidebarContainer.Init()
204+ return p.layout.SetRightPanel(sidebarContainer)
142func (p *chatPage) clearMessages() tea.Cmd {
207func (p *chatPage) clearMessages() tea.Cmd {
143 return p.layout.ClearLeftPanel()
208 return p.layout.ClearLeftPanel()
211+func (p *chatPage) setCompactMode(compact bool) tea.Cmd {
212+ p.compactMode = 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...)
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))
228+ cmds = append(cmds, p.setSidebar())
229+ return tea.Batch(cmds...)
146func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
233func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
147 var cmds []tea.Cmd
234 var cmds []tea.Cmd
148 if p.session.ID == "" {
235 if p.session.ID == "" {
@@ -175,7 +262,21 @@ func (p *chatPage) GetSize() (int, int) {
177func (p *chatPage) View() tea.View {
264func (p *chatPage) View() tea.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
178− return p.layout.View()
267+ return p.layout.View()
269+ layoutView := p.layout.View()
270+ chatView := tea.NewView(
273+ p.header.View().String(),
274+ layoutView.String(),
278+ chatView.SetCursor(layoutView.Cursor())
181func (p *chatPage) Bindings() []key.Binding {
282func (p *chatPage) Bindings() []key.Binding {
@@ -207,22 +308,26 @@ func (p *chatPage) Bindings() []key.Binding {
207 return bindings
308 return bindings
210−func NewChatPage(app *app.App) ChatPage {
311+func sidebarCmp(app *app.App) layout.Container {
211− sidebarContainer := layout.NewContainer(
312+ return layout.NewContainer(
212 sidebar.NewSidebarCmp(app.History, app.LSPClients),
313 sidebar.NewSidebarCmp(app.History, app.LSPClients),
213 layout.WithPadding(1, 1, 1, 1),
314 layout.WithPadding(1, 1, 1, 1),
318+func NewChatPage(app *app.App) ChatPage {
215 editorContainer := layout.NewContainer(
319 editorContainer := layout.NewContainer(
216 editor.NewEditorCmp(app),
320 editor.NewEditorCmp(app),
218 return &chatPage{
322 return &chatPage{
219 app: app,
323 app: app,
220 layout: layout.NewSplitPane(
324 layout: layout.NewSplitPane(
221− layout.WithRightPanel(sidebarContainer),
325+ layout.WithRightPanel(sidebarCmp(app)),
222 layout.WithBottomPanel(editorContainer),
326 layout.WithBottomPanel(editorContainer),
223 layout.WithFixedBottomHeight(5),
327 layout.WithFixedBottomHeight(5),
224 layout.WithFixedRightWidth(31),
328 layout.WithFixedRightWidth(31),
226 keyMap: DefaultKeyMap(),
330 keyMap: DefaultKeyMap(),
331+ header: header.New(app.LSPClients),