@@ -5,6 +5,7 @@ import (
8 "math/rand"
9 "math/rand"
9 "net/http"
10 "net/http"
@@ -23,6 +24,7 @@ import (
23 "github.com/charmbracelet/catwalk/pkg/catwalk"
24 "github.com/charmbracelet/catwalk/pkg/catwalk"
24 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
25 "github.com/charmbracelet/crush/internal/agent/tools/mcp"
25 "github.com/charmbracelet/crush/internal/app"
26 "github.com/charmbracelet/crush/internal/app"
27+ "github.com/charmbracelet/crush/internal/commands"
26 "github.com/charmbracelet/crush/internal/config"
28 "github.com/charmbracelet/crush/internal/config"
27 "github.com/charmbracelet/crush/internal/filetracker"
29 "github.com/charmbracelet/crush/internal/filetracker"
28 "github.com/charmbracelet/crush/internal/history"
30 "github.com/charmbracelet/crush/internal/history"
@@ -46,6 +48,15 @@ import (
46 "github.com/charmbracelet/x/editor"
48 "github.com/charmbracelet/x/editor"
51+// Compact mode breakpoints.
53+ compactModeWidthBreakpoint = 120
54+ compactModeHeightBreakpoint = 30
57+// Session details panel max height.
58+const sessionDetailsMaxHeight = 20
49// uiFocusState represents the current focus state of the UI.
60// uiFocusState represents the current focus state of the UI.
50type uiFocusState uint8
61type uiFocusState uint8
@@ -64,14 +75,24 @@ const (
64 uiInitialize
75 uiInitialize
70type openEditorMsg struct {
80type openEditorMsg struct {
71 Text string
81 Text string
85+ // cancelTimerExpiredMsg is sent when the cancel timer expires.
74−type cancelTimerExpiredMsg struct{}
86+ cancelTimerExpiredMsg struct{}
87+ // userCommandsLoadedMsg is sent when user commands are loaded.
88+ userCommandsLoadedMsg struct {
89+ Commands []commands.CustomCommand
91+ // mcpCustomCommandsLoadedMsg is sent when mcp prompts are loaded.
92+ mcpCustomCommandsLoadedMsg struct {
93+ Prompts []commands.MCPCustomCommand
76// UI represents the main user interface model.
97// UI represents the main user interface model.
77type UI struct {
98type UI struct {
@@ -142,6 +163,20 @@ type UI struct {
143 // imgCaps stores the terminal image capabilities.
164 // imgCaps stores the terminal image capabilities.
144 imgCaps timage.Capabilities
165 imgCaps timage.Capabilities
167+ // custom commands & mcp commands
168+ customCommands []commands.CustomCommand
169+ mcpCustomCommands []commands.MCPCustomCommand
171+ // forceCompactMode tracks whether compact mode is forced by user toggle
172+ forceCompactMode bool
174+ // isCompact tracks whether we're currently in compact layout mode (either
175+ // by user toggle or auto-switch based on window size)
178+ // detailsOpen tracks whether the details panel is open (in compact mode)
147// New creates a new instance of the [UI] model.
182// New creates a new instance of the [UI] model.
@@ -214,6 +249,9 @@ func New(com *common.Common) *UI {
214 ui.textarea.Placeholder = ui.readyPlaceholder
249 ui.textarea.Placeholder = ui.readyPlaceholder
215 ui.status = status
250 ui.status = status
252+ // Initialize compact mode from config
253+ ui.forceCompactMode = com.Config().Options.TUI.CompactMode
217 return ui
255 return ui
@@ -228,9 +266,37 @@ func (m *UI) Init() tea.Cmd {
228 // sequences.
266 // sequences.
229 cmds = append(cmds, timage.RequestCapabilities())
267 cmds = append(cmds, timage.RequestCapabilities())
269+ // load the user commands async
270+ cmds = append(cmds, m.loadCustomCommands())
231 return tea.Batch(cmds...)
271 return tea.Batch(cmds...)
274+// loadCustomCommands loads the custom commands asynchronously.
275+func (m *UI) loadCustomCommands() tea.Cmd {
276+ return func() tea.Msg {
277+ customCommands, err := commands.LoadCustomCommands(m.com.Config())
279+ slog.Error("failed to load custom commands", "error", err)
281+ return userCommandsLoadedMsg{Commands: customCommands}
285+// loadMCPrompts loads the MCP prompts asynchronously.
286+func (m *UI) loadMCPrompts() tea.Cmd {
287+ return func() tea.Msg {
288+ prompts, err := commands.LoadMCPCustomCommands()
290+ slog.Error("failed to load mcp prompts", "error", err)
293+ // flag them as loaded even if there is none or an error
294+ prompts = []commands.MCPCustomCommand{}
296+ return mcpCustomCommandsLoadedMsg{Prompts: prompts}
234// Update handles updates to the UI model.
300// Update handles updates to the UI model.
235func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
301func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
236 var cmds []tea.Cmd
302 var cmds []tea.Cmd
@@ -242,6 +308,9 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
243 case loadSessionMsg:
309 case loadSessionMsg:
244 m.state = uiChat
310 m.state = uiChat
311+ if m.forceCompactMode {
245 m.session = msg.session
314 m.session = msg.session
246 m.sessionFiles = msg.files
315 m.sessionFiles = msg.files
247 msgs, err := m.com.App.Messages.List(context.Background(), m.session.ID)
316 msgs, err := m.com.App.Messages.List(context.Background(), m.session.ID)
@@ -253,6 +322,29 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
253 cmds = append(cmds, cmd)
322 cmds = append(cmds, cmd)
325+ case userCommandsLoadedMsg:
326+ m.customCommands = msg.Commands
327+ dia := m.dialog.Dialog(dialog.CommandsID)
332+ commands, ok := dia.(*dialog.Commands)
334+ commands.SetCustomCommands(m.customCommands)
336+ case mcpCustomCommandsLoadedMsg:
337+ m.mcpCustomCommands = msg.Prompts
338+ dia := m.dialog.Dialog(dialog.CommandsID)
343+ commands, ok := dia.(*dialog.Commands)
345+ commands.SetMCPCustomCommands(m.mcpCustomCommands)
256 case pubsub.Event[message.Message]:
348 case pubsub.Event[message.Message]:
257 // Check if this is a child session message for an agent tool.
349 // Check if this is a child session message for an agent tool.
258 if m.session == nil {
350 if m.session == nil {
@@ -277,18 +369,16 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
277 m.lspStates = app.GetLSPStates()
369 m.lspStates = app.GetLSPStates()
278 case pubsub.Event[mcp.Event]:
370 case pubsub.Event[mcp.Event]:
279 m.mcpStates = mcp.GetStates()
371 m.mcpStates = mcp.GetStates()
280− if msg.Type == pubsub.UpdatedEvent && m.dialog.ContainsDialog(dialog.CommandsID) {
372+ // check if all mcps are initialized
281− dia := m.dialog.Dialog(dialog.CommandsID)
373+ initialized := true
282− if dia == nil {
374+ for _, state := range m.mcpStates {
375+ if state.State == mcp.StateStarting {
286− commands, ok := dia.(*dialog.Commands)
380+ if initialized && m.mcpCustomCommands == nil {
288− if cmd := commands.ReloadMCPPrompts(); cmd != nil {
289− cmds = append(cmds, cmd)
381+ cmds = append(cmds, m.loadMCPrompts())
293 case pubsub.Event[permission.PermissionRequest]:
383 case pubsub.Event[permission.PermissionRequest]:
294 if cmd := m.openPermissionsDialog(msg.Payload); cmd != nil {
384 if cmd := m.openPermissionsDialog(msg.Payload); cmd != nil {
@@ -307,6 +397,7 @@ func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
307 return m, nil
397 return m, nil
308 case tea.WindowSizeMsg:
398 case tea.WindowSizeMsg:
309 m.width, m.height = msg.Width, msg.Height
399 m.width, m.height = msg.Width, msg.Height
400+ m.handleCompactMode(m.width, m.height)
310 m.updateLayoutAndSize()
401 m.updateLayoutAndSize()
311 // XXX: We need to store cell dimensions for image rendering.
402 // XXX: We need to store cell dimensions for image rendering.
312 m.imgCaps.Columns, m.imgCaps.Rows = msg.Width, msg.Height
403 m.imgCaps.Columns, m.imgCaps.Rows = msg.Width, msg.Height
@@ -789,8 +880,18 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
789 case dialog.ActionToggleHelp:
880 case dialog.ActionToggleHelp:
790 m.status.ToggleHelp()
881 m.status.ToggleHelp()
791 m.dialog.CloseDialog(dialog.CommandsID)
882 m.dialog.CloseDialog(dialog.CommandsID)
883+ case dialog.ActionToggleCompactMode:
884+ cmds = append(cmds, m.toggleCompactMode())
885+ m.dialog.CloseDialog(dialog.CommandsID)
792 case dialog.ActionQuit:
886 case dialog.ActionQuit:
793 cmds = append(cmds, tea.Quit)
887 cmds = append(cmds, tea.Quit)
888+ case dialog.ActionInitializeProject:
889+ if m.com.App.AgentCoordinator != nil && m.com.App.AgentCoordinator.IsBusy() {
890+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before summarizing session..."))
893+ cmds = append(cmds, m.initializeProject())
794 case dialog.ActionSelectModel:
895 case dialog.ActionSelectModel:
795 if m.com.App.AgentCoordinator.IsBusy() {
896 if m.com.App.AgentCoordinator.IsBusy() {
796 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
897 cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
@@ -890,6 +991,10 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
890 cmds = append(cmds, cmd)
991 cmds = append(cmds, cmd)
892 return true
993 return true
994+ case key.Matches(msg, m.keyMap.Chat.Details) && m.isCompact:
995+ m.detailsOpen = !m.detailsOpen
996+ m.updateLayoutAndSize()
894 return false
999 return false
@@ -924,7 +1029,7 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
924 case uiInitialize:
1029 case uiInitialize:
925 cmds = append(cmds, m.updateInitializeView(msg)...)
1030 cmds = append(cmds, m.updateInitializeView(msg)...)
926 return tea.Batch(cmds...)
1031 return tea.Batch(cmds...)
927− case uiChat, uiLanding, uiChatCompact:
1032+ case uiChat, uiLanding:
928 switch m.focus {
1033 switch m.focus {
929 case uiFocusEditor:
1034 case uiFocusEditor:
930 // Handle completions if open.
1035 // Handle completions if open.
@@ -1027,6 +1132,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1135+ // remove the details if they are open when user starts typing
1137+ m.detailsOpen = false
1138+ m.updateLayoutAndSize()
1030 ta, cmd := m.textarea.Update(msg)
1141 ta, cmd := m.textarea.Update(msg)
1031 m.textarea = ta
1142 m.textarea = ta
1032 cmds = append(cmds, cmd)
1143 cmds = append(cmds, cmd)
@@ -1177,28 +1288,26 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1177 editor.Draw(scr, layout.editor)
1288 editor.Draw(scr, layout.editor)
1179 case uiChat:
1290 case uiChat:
1292+ header := uv.NewStyledString(m.header)
1180− m.chat.Draw(scr, layout.main)
1293+ header.Draw(scr, layout.header)
1295+ m.drawSidebar(scr, layout.sidebar)
1182− header := uv.NewStyledString(m.header)
1183− header.Draw(scr, layout.header)
1298+ m.chat.Draw(scr, layout.main)
1184− m.drawSidebar(scr, layout.sidebar)
1186− editor := uv.NewStyledString(m.renderEditorView(scr.Bounds().Dx() - layout.sidebar.Dx()))
1300+ editorWidth := scr.Bounds().Dx()
1302+ editorWidth -= layout.sidebar.Dx()
1304+ editor := uv.NewStyledString(m.renderEditorView(editorWidth))
1187 editor.Draw(scr, layout.editor)
1305 editor.Draw(scr, layout.editor)
1189− case uiChatCompact:
1307+ // Draw details overlay in compact mode when open
1190− header := uv.NewStyledString(m.header)
1308+ if m.isCompact && m.detailsOpen {
1191− header.Draw(scr, layout.header)
1309+ m.drawSessionDetails(scr, layout.sessionDetails)
1193− mainView := lipgloss.NewStyle().Width(layout.main.Dx()).
1194− Height(layout.main.Dy()).
1195− Background(lipgloss.ANSIColor(rand.Intn(256))).
1196− Render(" Compact Chat Messages ")
1197− main := uv.NewStyledString(mainView)
1198− main.Draw(scr, layout.main)
1200− editor := uv.NewStyledString(m.renderEditorView(scr.Bounds().Dx()))
1201− editor.Draw(scr, layout.editor)
1204 // Add status and help layer
1313 // Add status and help layer
@@ -1247,6 +1356,10 @@ func (m *UI) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
1247 // Don't show cursor if editor is not visible
1356 // Don't show cursor if editor is not visible
1248 return nil
1357 return nil
1359+ if m.detailsOpen && m.isCompact {
1360+ // Don't show cursor if details overlay is open
1251 if m.textarea.Focused() {
1364 if m.textarea.Focused() {
1252 cur := m.textarea.Cursor()
1365 cur := m.textarea.Cursor()
@@ -1494,6 +1607,36 @@ func (m *UI) FullHelp() [][]key.Binding {
1494 return binds
1607 return binds
1610+// toggleCompactMode toggles compact mode between uiChat and uiChatCompact states.
1611+func (m *UI) toggleCompactMode() tea.Cmd {
1612+ m.forceCompactMode = !m.forceCompactMode
1614+ err := m.com.Config().SetCompactMode(m.forceCompactMode)
1616+ return uiutil.ReportError(err)
1619+ m.handleCompactMode(m.width, m.height)
1620+ m.updateLayoutAndSize()
1625+// handleCompactMode updates the UI state based on window size and compact mode setting.
1626+func (m *UI) handleCompactMode(newWidth, newHeight int) {
1627+ if m.state == uiChat {
1628+ if m.forceCompactMode {
1632+ if newWidth < compactModeWidthBreakpoint || newHeight < compactModeHeightBreakpoint {
1635+ m.isCompact = false
1497// updateLayoutAndSize updates the layout and sizes of UI components.
1640// updateLayoutAndSize updates the layout and sizes of UI components.
1498func (m *UI) updateLayoutAndSize() {
1641func (m *UI) updateLayoutAndSize() {
1499 m.layout = m.generateLayout(m.width, m.height)
1642 m.layout = m.generateLayout(m.width, m.height)
@@ -1515,11 +1658,11 @@ func (m *UI) updateSize() {
1515 m.renderHeader(false, m.layout.header.Dx())
1658 m.renderHeader(false, m.layout.header.Dx())
1517 case uiChat:
1660 case uiChat:
1518− m.renderSidebarLogo(m.layout.sidebar.Dx())
1661+ if m.isCompact {
1520− case uiChatCompact:
1521− // TODO: set the width and heigh of the chat component
1522− m.renderHeader(true, m.layout.header.Dx())
1662+ m.renderHeader(true, m.layout.header.Dx())
1664+ m.renderSidebarLogo(m.layout.sidebar.Dx())
@@ -1536,8 +1679,7 @@ func (m *UI) generateLayout(w, h int) layout {
1536 // The sidebar width
1679 // The sidebar width
1537 sidebarWidth := 30
1680 sidebarWidth := 30
1538 // The header height
1681 // The header height
1539− // TODO: handle compact
1682+ const landingHeaderHeight = 4
1542 var helpKeyMap help.KeyMap = m
1684 var helpKeyMap help.KeyMap = m
1543 if m.status.ShowingAll() {
1685 if m.status.ShowingAll() {
@@ -1576,7 +1718,7 @@ func (m *UI) generateLayout(w, h int) layout {
1576 // ------
1718 // ------
1579− headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
1721+ headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(landingHeaderHeight))
1580 layout.header = headerRect
1722 layout.header = headerRect
1581 layout.main = mainRect
1723 layout.main = mainRect
@@ -1590,7 +1732,7 @@ func (m *UI) generateLayout(w, h int) layout {
1590 // editor
1732 // editor
1591 // ------
1733 // ------
1593− headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(headerHeight))
1735+ headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(landingHeaderHeight))
1594 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1736 mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1595 // Remove extra padding from editor (but keep it for header and main)
1737 // Remove extra padding from editor (but keep it for header and main)
1596 editorRect.Min.X -= 1
1738 editorRect.Min.X -= 1
@@ -1600,41 +1742,52 @@ func (m *UI) generateLayout(w, h int) layout {
1600 layout.editor = editorRect
1742 layout.editor = editorRect
1602 case uiChat:
1744 case uiChat:
1603− // Layout
1746+ // Layout
1755+ const compactHeaderHeight = 1
1756+ headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(compactHeaderHeight))
1757+ detailsHeight := min(sessionDetailsMaxHeight, area.Dy()-1) // One row for the header
1758+ sessionDetailsArea, _ := uv.SplitVertical(appRect, uv.Fixed(detailsHeight))
1759+ layout.sessionDetails = sessionDetailsArea
1760+ layout.sessionDetails.Min.Y += compactHeaderHeight // adjust for header
1761+ // Add one line gap between header and main content
1762+ mainRect.Min.Y += 1
1763+ mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1764+ mainRect.Max.X -= 1 // Add padding right
1765+ // Add bottom margin to main
1766+ mainRect.Max.Y -= 1
1767+ layout.header = headerRect
1768+ layout.main = mainRect
1769+ layout.editor = editorRect
1605− // ------|---
1773+ // ------|---
1606− // main |
1774+ // main |
1607− // ------| side
1775+ // ------| side
1608− // editor|
1776+ // editor|
1609− // ----------
1777+ // ----------
1610− // help
1778+ // help
1612− mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth))
1780+ mainRect, sideRect := uv.SplitHorizontal(appRect, uv.Fixed(appRect.Dx()-sidebarWidth))
1613− // Add padding left
1781+ // Add padding left
1614− sideRect.Min.X += 1
1782+ sideRect.Min.X += 1
1615− mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1783+ mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1616− mainRect.Max.X -= 1 // Add padding right
1784+ mainRect.Max.X -= 1 // Add padding right
1617− // Add bottom margin to main
1785+ // Add bottom margin to main
1618− mainRect.Max.Y -= 1
1786+ mainRect.Max.Y -= 1
1619− layout.sidebar = sideRect
1787+ layout.sidebar = sideRect
1620− layout.main = mainRect
1788+ layout.main = mainRect
1621− layout.editor = editorRect
1789+ layout.editor = editorRect
1623− case uiChatCompact:
1633− headerRect, mainRect := uv.SplitVertical(appRect, uv.Fixed(appRect.Dy()-headerHeight))
1634− mainRect, editorRect := uv.SplitVertical(mainRect, uv.Fixed(mainRect.Dy()-editorHeight))
1635− layout.header = headerRect
1636− layout.main = mainRect
1637− layout.editor = editorRect
1640 if !layout.editor.Empty() {
1793 if !layout.editor.Empty() {
@@ -1668,6 +1821,9 @@ type layout struct {
1669 // status is the area for the status view.
1822 // status is the area for the status view.
1670 status uv.Rectangle
1823 status uv.Rectangle
1825+ // session details is the area for the session details overlay in compact mode.
1826+ sessionDetails uv.Rectangle
1673func (m *UI) openEditor(value string) tea.Cmd {
1829func (m *UI) openEditor(value string) tea.Cmd {
@@ -1873,8 +2029,11 @@ func (m *UI) renderEditorView(width int) string {
1874// renderHeader renders and caches the header logo at the specified width.
2030// renderHeader renders and caches the header logo at the specified width.
1875func (m *UI) renderHeader(compact bool, width int) {
2031func (m *UI) renderHeader(compact bool, width int) {
1876− // TODO: handle the compact case differently
2032+ if compact && m.session != nil && m.com.App != nil {
2033+ m.header = renderCompactHeader(m.com, m.session, m.com.App.LSPClients, m.detailsOpen, width)
1877− m.header = renderLogo(m.com.Styles, compact, width)
2035+ m.header = renderLogo(m.com.Styles, compact, width)
1880// renderSidebarLogo renders and caches the sidebar logo at the specified
2039// renderSidebarLogo renders and caches the sidebar logo at the specified
@@ -1896,8 +2055,13 @@ func (m *UI) sendMessage(content string, attachments []message.Attachment) tea.C
1896 return uiutil.ReportError(err)
2055 return uiutil.ReportError(err)
1898 m.state = uiChat
2057 m.state = uiChat
2058+ if m.forceCompactMode {
2061+ if newSession.ID != "" {
1899− m.session = &newSession
2062+ m.session = &newSession
1900− cmds = append(cmds, m.loadSession(newSession.ID))
2063+ cmds = append(cmds, m.loadSession(newSession.ID))
1903 // Capture session ID to avoid race with main goroutine updating m.session.
2067 // Capture session ID to avoid race with main goroutine updating m.session.
@@ -2031,7 +2195,7 @@ func (m *UI) openCommandsDialog() tea.Cmd {
2031 sessionID = m.session.ID
2195 sessionID = m.session.ID
2034− commands, err := dialog.NewCommands(m.com, sessionID)
2198+ commands, err := dialog.NewCommands(m.com, sessionID, m.customCommands, m.mcpCustomCommands)
2035 if err != nil {
2199 if err != nil {
2036 return uiutil.ReportError(err)
2200 return uiutil.ReportError(err)
@@ -2228,6 +2392,56 @@ func (m *UI) pasteIdx() int {
2228 return result + 1
2392 return result + 1
2395+// drawSessionDetails draws the session details in compact mode.
2396+func (m *UI) drawSessionDetails(scr uv.Screen, area uv.Rectangle) {
2397+ if m.session == nil {
2403+ width := area.Dx() - s.CompactDetails.View.GetHorizontalFrameSize()
2404+ height := area.Dy() - s.CompactDetails.View.GetVerticalFrameSize()
2406+ title := s.CompactDetails.Title.Width(width).MaxHeight(2).Render(m.session.Title)
2407+ blocks := []string{
2410+ m.modelInfo(width),
2414+ detailsHeader := lipgloss.JoinVertical(
2419+ version := s.CompactDetails.Version.Foreground(s.Border).Width(width).AlignHorizontal(lipgloss.Right).Render(version.Version)
2421+ remainingHeight := height - lipgloss.Height(detailsHeader) - lipgloss.Height(version)
2423+ const maxSectionWidth = 50
2424+ sectionWidth := min(maxSectionWidth, width/3-2) // account for 2 spaces
2425+ maxItemsPerSection := remainingHeight - 3 // Account for section title and spacing
2427+ lspSection := m.lspInfo(sectionWidth, maxItemsPerSection, false)
2428+ mcpSection := m.mcpInfo(sectionWidth, maxItemsPerSection, false)
2429+ filesSection := m.filesInfo(m.com.Config().WorkingDir(), sectionWidth, maxItemsPerSection, false)
2430+ sections := lipgloss.JoinHorizontal(lipgloss.Top, filesSection, " ", lspSection, " ", mcpSection)
2431+ uv.NewStyledString(
2432+ s.CompactDetails.View.
2435+ lipgloss.JoinVertical(
2231// renderLogo renders the Crush logo with the given styles and dimensions.
2445// renderLogo renders the Crush logo with the given styles and dimensions.
2232func renderLogo(t *styles.Styles, compact bool, width int) string {
2446func renderLogo(t *styles.Styles, compact bool, width int) string {
2233 return logo.Render(version.Version, compact, logo.Opts{
2447 return logo.Render(version.Version, compact, logo.Opts{