diff --git a/internal/tui/components/chat/header/header.go b/internal/tui/components/chat/header/header.go index c88ef849c87310fdf2327172399e5e0fa79d1528..87939b3faae8cd9863f055df6b4a7e4ee83a3467 100644 --- a/internal/tui/components/chat/header/header.go +++ b/internal/tui/components/chat/header/header.go @@ -19,6 +19,7 @@ import ( type Header interface { util.Model SetSession(session session.Session) + SetDetailsOpen(open bool) } type header struct { @@ -119,6 +120,10 @@ func (h *header) details() string { return strings.Join(parts, dot) } +func (h *header) SetDetailsOpen(open bool) { + h.detailsOpen = open +} + // SetSession implements Header. func (h *header) SetSession(session session.Session) { h.session = session diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index 19befb2ba16c68624b04a8c101521ef9cda6c9fd..54d9cb6b3ab78aa0d673a6c143b2119d73d08d7f 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -60,15 +60,17 @@ type sidebarCmp struct { logo string cwd string lspClients map[string]*lsp.Client + compactMode bool history history.Service // Using a sync map here because we might receive file history events concurrently files sync.Map } -func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client) Sidebar { +func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client, compact bool) Sidebar { return &sidebarCmp{ - lspClients: lspClients, - history: history, + lspClients: lspClients, + history: history, + compactMode: compact, } } @@ -109,17 +111,24 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *sidebarCmp) View() tea.View { t := styles.CurrentTheme() - parts := []string{ - m.logo, + parts := []string{} + if !m.compactMode { + parts = append(parts, m.logo) } - if m.session.ID != "" { + if !m.compactMode && m.session.ID != "" { parts = append(parts, t.S().Muted.Render(m.session.Title), "") + } else if m.session.ID != "" { + parts = append(parts, t.S().Text.Render(m.session.Title), "") } + if !m.compactMode { + parts = append(parts, + m.cwd, + "", + ) + } parts = append(parts, - m.cwd, - "", m.currentModelBlock(), ) if m.session.ID != "" { diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 1f0d5550b4c2dfd56b3b8152aca50d3a3ce1a505..4985cdc9bca2fcd7979e3a1b5f674e34272d189e 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -18,7 +18,9 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/core/layout" "github.com/charmbracelet/crush/internal/tui/components/dialogs/commands" "github.com/charmbracelet/crush/internal/tui/page" + "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" + "github.com/charmbracelet/lipgloss/v2" ) var ChatPageID page.PageID = "chat" @@ -51,7 +53,9 @@ type chatPage struct { compactMode bool forceCompactMode bool // Force compact mode regardless of window size + showDetails bool // Show details in the header header header.Header + compactSidebar layout.Container } func (p *chatPage) Init() tea.Cmd { @@ -69,6 +73,9 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { h, cmd := p.header.Update(msg) cmds = append(cmds, cmd) p.header = h.(header.Header) + if p.compactMode && p.showDetails { + cmds = append(cmds, p.compactSidebar.SetSize(msg.Width-4, 0)) + } // the mode is only relevant when there is a session if p.session.ID != "" { // Only auto-switch to compact mode if not forced @@ -178,15 +185,34 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { p.app.CoderAgent.Cancel(p.session.ID) return p, nil } + case key.Matches(msg, p.keyMap.Details): + if p.session.ID == "" || !p.compactMode { + return p, nil // No session to show details for + } + p.showDetails = !p.showDetails + p.header.SetDetailsOpen(p.showDetails) + if p.showDetails { + p.compactSidebar = sidebarCmp(p.app, true) + c, cmd := p.compactSidebar.Update(chat.SessionSelectedMsg(p.session)) + p.compactSidebar = c.(layout.Container) + return p, tea.Batch( + cmd, + p.compactSidebar.SetSize(p.wWidth-4, 0), + ) + } + + return p, nil } } u, cmd := p.layout.Update(msg) cmds = append(cmds, cmd) p.layout = u.(layout.SplitPaneLayout) - h, cmd := p.header.Update(msg) - cmds = append(cmds, cmd) - p.header = h.(header.Header) + if p.compactMode && p.showDetails { + s, cmd := p.compactSidebar.Update(msg) + p.compactSidebar = s.(layout.Container) + cmds = append(cmds, cmd) + } return p, tea.Batch(cmds...) } @@ -199,7 +225,7 @@ func (p *chatPage) setMessages() tea.Cmd { } func (p *chatPage) setSidebar() tea.Cmd { - sidebarContainer := sidebarCmp(p.app) + sidebarContainer := sidebarCmp(p.app, false) sidebarContainer.Init() return p.layout.SetRightPanel(sidebarContainer) } @@ -267,16 +293,29 @@ func (p *chatPage) View() tea.View { return p.layout.View() } layoutView := p.layout.View() - chatView := tea.NewView( - strings.Join( - []string{ - p.header.View().String(), - layoutView.String(), - }, "\n", - ), + chatView := strings.Join( + []string{ + p.header.View().String(), + layoutView.String(), + }, "\n", + ) + layers := []*lipgloss.Layer{ + lipgloss.NewLayer(chatView).X(0).Y(0), + } + if p.showDetails { + t := styles.CurrentTheme() + style := t.S().Base. + Border(lipgloss.RoundedBorder()). + BorderForeground(t.BorderFocus) + details := style.Render(p.compactSidebar.View().String()) + layers = append(layers, lipgloss.NewLayer(details).X(1).Y(1)) + } + canvas := lipgloss.NewCanvas( + layers..., ) - chatView.SetCursor(layoutView.Cursor()) - return chatView + view := tea.NewView(canvas.Render()) + view.SetCursor(layoutView.Cursor()) + return view } func (p *chatPage) Bindings() []key.Binding { @@ -308,10 +347,14 @@ func (p *chatPage) Bindings() []key.Binding { return bindings } -func sidebarCmp(app *app.App) layout.Container { +func sidebarCmp(app *app.App, compact bool) layout.Container { + padding := layout.WithPadding(1, 1, 1, 1) + if compact { + padding = layout.WithPadding(0, 1, 1, 1) + } return layout.NewContainer( - sidebar.NewSidebarCmp(app.History, app.LSPClients), - layout.WithPadding(1, 1, 1, 1), + sidebar.NewSidebarCmp(app.History, app.LSPClients, compact), + padding, ) } @@ -322,7 +365,7 @@ func NewChatPage(app *app.App) ChatPage { return &chatPage{ app: app, layout: layout.NewSplitPane( - layout.WithRightPanel(sidebarCmp(app)), + layout.WithRightPanel(sidebarCmp(app, false)), layout.WithBottomPanel(editorContainer), layout.WithFixedBottomHeight(5), layout.WithFixedRightWidth(31), diff --git a/internal/tui/page/chat/keys.go b/internal/tui/page/chat/keys.go index 87167ee2c47162bb9c7c4111d100bd80283e5bbe..ef896aaab10fe36ee8ce88d3f70a3f03e3c61d3e 100644 --- a/internal/tui/page/chat/keys.go +++ b/internal/tui/page/chat/keys.go @@ -9,6 +9,7 @@ type KeyMap struct { AddAttachment key.Binding Cancel key.Binding Tab key.Binding + Details key.Binding } func DefaultKeyMap() KeyMap { @@ -29,5 +30,9 @@ func DefaultKeyMap() KeyMap { key.WithKeys("tab"), key.WithHelp("tab", "change focus"), ), + Details: key.NewBinding( + key.WithKeys("ctrl+d"), + key.WithHelp("ctrl+d", "toggle details"), + ), } }