on main help sign in
Merge pull request #1941 from charmbracelet/small_ui_fixes

Small fixes to the new UI

commit15b841b42d4bc2da0d2376db32f2a31c487a6789 authorAyman Bagabas <ayman.bagabas@gmail.com> dateThu 22 Jan 2026, 10:01 (9 months ago) parent40bfd6e1fad83b
8 files changed+80−18
internal/agent/coordinator.go+5 internal/ui/dialog/commands.go+8−3 internal/ui/dialog/models.go+4−1 internal/ui/dialog/sessions.go+8−1 internal/ui/model/keys.go+2−2 internal/ui/model/lsp.go+8−1 internal/ui/model/onboarding.go+19−6 internal/ui/model/ui.go+26−4
internal/agent/coordinator.go+5
@@ -117,6 +117,11 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
117117 return nil, err
118118 }
119119
120+ // refresh models before each run
121+ if err := c.UpdateModels(ctx); err != nil {
122+ return nil, fmt.Errorf("failed to update models: %w", err)
123+ }
124+
120125 model := c.currentAgent.Model()
121126 maxTokens := model.CatwalkCfg.DefaultMaxTokens
122127 if model.ModelCfg.MaxTokens != 0 {
internal/ui/dialog/commands.go+8−3
@@ -28,7 +28,10 @@ type CommandType uint
2828// String returns the string representation of the CommandType.
2929func (c CommandType) String() string { return []string{"System", "User", "MCP"}[c] }
3030
31−const sidebarCompactModeBreakpoint = 120
31+const (
32+ sidebarCompactModeBreakpoint = 120
33+ defaultCommandsDialogMaxWidth = 70
34+)
3235
3336const (
3437 SystemCommands CommandType = iota
@@ -238,7 +241,7 @@ func commandsRadioView(sty *styles.Styles, selected CommandType, hasUserCmds boo
238241// Draw implements [Dialog].
239242func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
240243 t := c.com.Styles
241− width := max(0, min(defaultDialogMaxWidth, area.Dx()))
244+ width := max(0, min(defaultCommandsDialogMaxWidth, area.Dx()))
242245 height := max(0, min(defaultDialogHeight, area.Dy()))
243246 if area.Dx() != c.windowWidth && c.selected == SystemCommands {
244247 c.windowWidth = area.Dx()
@@ -254,7 +257,9 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
254257 t.Dialog.View.GetVerticalFrameSize()
255258
256259 c.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
257− c.list.SetSize(innerWidth, height-heightOffset)
260+
261+ listHeight := min(height-heightOffset, c.list.Len())
262+ c.list.SetSize(innerWidth, listHeight)
258263 c.help.SetWidth(innerWidth)
259264
260265 rc := NewRenderContext(t, width)
internal/ui/dialog/models.go+4−1
@@ -69,6 +69,8 @@ const (
6969// ModelsID is the identifier for the model selection dialog.
7070const ModelsID = "models"
7171
72+const defaultModelsDialogMaxWidth = 70
73+
7274// Models represents a model selection dialog.
7375type Models struct {
7476 com *common.Common
@@ -240,7 +242,7 @@ func (m *Models) modelTypeRadioView() string {
240242// Draw implements [Dialog].
241243func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
242244 t := m.com.Styles
243− width := max(0, min(defaultDialogMaxWidth, area.Dx()))
245+ width := max(0, min(defaultModelsDialogMaxWidth, area.Dx()))
244246 height := max(0, min(defaultDialogHeight, area.Dy()))
245247 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
246248 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
@@ -442,6 +444,7 @@ func (m *Models) setProviderItems() error {
442444 // Set model groups in the list.
443445 m.list.SetGroups(groups...)
444446 m.list.SetSelectedItem(selectedItemID)
447+ m.list.ScrollToSelected()
445448
446449 // Update placeholder based on model type
447450 m.input.Placeholder = m.modelType.Placeholder()
internal/ui/dialog/sessions.go+8−1
@@ -57,7 +57,6 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
5757 s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
5858 s.list.Focus()
5959 s.list.SetSelected(s.selectedSessionInx)
60− s.list.ScrollToSelected()
6160
6261 s.input = textinput.New()
6362 s.input.SetVirtualCursor(false)
@@ -153,6 +152,14 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
153152 s.list.SetSize(innerWidth, height-heightOffset)
154153 s.help.SetWidth(innerWidth)
155154
155+ // This makes it so we do not scroll the list if we don't have to
156+ start, end := s.list.VisibleItemIndices()
157+
158+ // if selected index is outside visible range, scroll to it
159+ if s.selectedSessionInx < start || s.selectedSessionInx > end {
160+ s.list.ScrollToSelected()
161+ }
162+
156163 rc := NewRenderContext(t, width)
157164 rc.Title = "Switch Session"
158165 inputView := t.Dialog.InputPrompt.Render(s.input.View())
internal/ui/model/keys.go+2−2
@@ -166,11 +166,11 @@ func DefaultKeyMap() KeyMap {
166166 )
167167
168168 km.Chat.Down = key.NewBinding(
169− key.WithKeys("down", "ctrl+j", "ctrl+n", "j"),
169+ key.WithKeys("down", "ctrl+j", "j"),
170170 key.WithHelp("↓", "down"),
171171 )
172172 km.Chat.Up = key.NewBinding(
173− key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
173+ key.WithKeys("up", "ctrl+k", "k"),
174174 key.WithHelp("↑", "up"),
175175 )
176176 km.Chat.UpDown = key.NewBinding(
internal/ui/model/lsp.go+8−1
@@ -23,8 +23,14 @@ type LSPInfo struct {
2323func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
2424 var lsps []LSPInfo
2525 t := m.com.Styles
26+ lspConfigs := m.com.Config().LSP.Sorted()
27+
28+ for _, cfg := range lspConfigs {
29+ state, ok := m.lspStates[cfg.Name]
30+ if !ok {
31+ continue
32+ }
2633
27− for _, state := range m.lspStates {
2834 client, ok := m.com.App.LSPClients.Get(state.Name)
2935 if !ok {
3036 continue
@@ -39,6 +45,7 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
3945
4046 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
4147 }
48+
4249 title := t.Subtle.Render("LSPs")
4350 if isSection {
4451 title = common.Section(t, title, width)
internal/ui/model/onboarding.go+19−6
@@ -8,9 +8,12 @@ import (
88 "charm.land/bubbles/v2/key"
99 tea "charm.land/bubbletea/v2"
1010 "charm.land/lipgloss/v2"
11+
12+ "github.com/charmbracelet/crush/internal/agent"
1113 "github.com/charmbracelet/crush/internal/config"
1214 "github.com/charmbracelet/crush/internal/home"
1315 "github.com/charmbracelet/crush/internal/ui/common"
16+ "github.com/charmbracelet/crush/internal/uiutil"
1417)
1518
1619// markProjectInitialized marks the current project as initialized in the config.
@@ -44,12 +47,22 @@ func (m *UI) updateInitializeView(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
4447
4548// initializeProject starts project initialization and transitions to the landing view.
4649func (m *UI) initializeProject() tea.Cmd {
47− // TODO: initialize the project
48− // for now we just go to the landing page
49− m.state = uiLanding
50− m.focus = uiFocusEditor
51− // TODO: actually send a message to the agent
52− return m.markProjectInitialized
50+ // clear the session
51+ m.newSession()
52+ cfg := m.com.Config()
53+ var cmds []tea.Cmd
54+
55+ initialize := func() tea.Msg {
56+ initPrompt, err := agent.InitializePrompt(*cfg)
57+ if err != nil {
58+ return uiutil.InfoMsg{Type: uiutil.InfoTypeError, Msg: err.Error()}
59+ }
60+ return sendMessageMsg{Content: initPrompt}
61+ }
62+ // Mark the project as initialized
63+ cmds = append(cmds, initialize, m.markProjectInitialized)
64+
65+ return tea.Sequence(cmds...)
5366}
5467
5568// skipInitializeProject skips project initialization and transitions to the landing view.
internal/ui/model/ui.go+26−4
@@ -265,6 +265,8 @@ func New(com *common.Common) *UI {
265265 completions: comp,
266266 attachments: attachments,
267267 todoSpinner: todoSpinner,
268+ lspStates: make(map[string]app.LSPClientInfo),
269+ mcpStates: make(map[string]mcp.ClientInfo),
268270 }
269271
270272 status := NewStatus(com, ui)
@@ -1106,6 +1108,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
11061108 break
11071109 }
11081110 cmds = append(cmds, m.initializeProject())
1111+ m.dialog.CloseDialog(dialog.CommandsID)
11091112
11101113 case dialog.ActionSelectModel:
11111114 if m.isAgentBusy() {
@@ -1329,6 +1332,13 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
13291332 }
13301333 return true
13311334 }
1335+ case key.Matches(msg, m.keyMap.Suspend):
1336+ if m.isAgentBusy() {
1337+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
1338+ return true
1339+ }
1340+ cmds = append(cmds, tea.Suspend)
1341+ return true
13321342 }
13331343 return false
13341344 }
@@ -1430,10 +1440,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
14301440 }
14311441 m.newSession()
14321442 case key.Matches(msg, m.keyMap.Tab):
1433− m.focus = uiFocusMain
1434− m.textarea.Blur()
1435− m.chat.Focus()
1436− m.chat.SetSelected(m.chat.Len() - 1)
1443+ if m.state != uiLanding {
1444+ m.focus = uiFocusMain
1445+ m.textarea.Blur()
1446+ m.chat.Focus()
1447+ m.chat.SetSelected(m.chat.Len() - 1)
1448+ }
14371449 case key.Matches(msg, m.keyMap.Editor.OpenEditor):
14381450 if m.isAgentBusy() {
14391451 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
@@ -1506,6 +1518,16 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
15061518 m.focus = uiFocusEditor
15071519 cmds = append(cmds, m.textarea.Focus())
15081520 m.chat.Blur()
1521+ case key.Matches(msg, m.keyMap.Chat.NewSession):
1522+ if !m.hasSession() {
1523+ break
1524+ }
1525+ if m.isAgentBusy() {
1526+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
1527+ break
1528+ }
1529+ m.focus = uiFocusEditor
1530+ m.newSession()
15091531 case key.Matches(msg, m.keyMap.Chat.Expand):
15101532 m.chat.ToggleExpandedSelectedItem()
15111533 case key.Matches(msg, m.keyMap.Chat.Up):