feat: add details overlay

Kujtim Hoxha created

Change summary

internal/tui/components/chat/header/header.go   |  5 +
internal/tui/components/chat/sidebar/sidebar.go | 25 ++++-
internal/tui/page/chat/chat.go                  | 77 ++++++++++++++----
internal/tui/page/chat/keys.go                  |  5 +
4 files changed, 87 insertions(+), 25 deletions(-)

Detailed changes

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

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 != "" {

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),

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"),
+		),
 	}
 }