on main help sign in
Merge pull request #1941 from charmbracelet/small_ui_fixes
15b841bAyman Bagabas
internal/agent/coordinator.go+5
@@ -117,6 +117,11 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
117 return nil, err
117 return nil, err
118 }
118 }
119
119
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+
120 model := c.currentAgent.Model()
125 model := c.currentAgent.Model()
121 maxTokens := model.CatwalkCfg.DefaultMaxTokens
126 maxTokens := model.CatwalkCfg.DefaultMaxTokens
122 if model.ModelCfg.MaxTokens != 0 {
127 if model.ModelCfg.MaxTokens != 0 {
internal/ui/dialog/commands.go+8−3
@@ -28,7 +28,10 @@ type CommandType uint
28// String returns the string representation of the CommandType.
28// String returns the string representation of the CommandType.
29func (c CommandType) String() string { return []string{"System", "User", "MCP"}[c] }
29func (c CommandType) String() string { return []string{"System", "User", "MCP"}[c] }
30
30
31+const (
31−const sidebarCompactModeBreakpoint = 120
32+ sidebarCompactModeBreakpoint = 120
33+ defaultCommandsDialogMaxWidth = 70
34+)
32
35
33const (
36const (
34 SystemCommands CommandType = iota
37 SystemCommands CommandType = iota
@@ -238,7 +241,7 @@ func commandsRadioView(sty *styles.Styles, selected CommandType, hasUserCmds boo
238// Draw implements [Dialog].
241// Draw implements [Dialog].
239func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
242func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
240 t := c.com.Styles
243 t := c.com.Styles
241− width := max(0, min(defaultDialogMaxWidth, area.Dx()))
244+ width := max(0, min(defaultCommandsDialogMaxWidth, area.Dx()))
242 height := max(0, min(defaultDialogHeight, area.Dy()))
245 height := max(0, min(defaultDialogHeight, area.Dy()))
243 if area.Dx() != c.windowWidth && c.selected == SystemCommands {
246 if area.Dx() != c.windowWidth && c.selected == SystemCommands {
244 c.windowWidth = area.Dx()
247 c.windowWidth = area.Dx()
@@ -254,7 +257,9 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
254 t.Dialog.View.GetVerticalFrameSize()
257 t.Dialog.View.GetVerticalFrameSize()
255
258
256 c.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
259 c.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
260+
257− c.list.SetSize(innerWidth, height-heightOffset)
261+ listHeight := min(height-heightOffset, c.list.Len())
262+ c.list.SetSize(innerWidth, listHeight)
258 c.help.SetWidth(innerWidth)
263 c.help.SetWidth(innerWidth)
259
264
260 rc := NewRenderContext(t, width)
265 rc := NewRenderContext(t, width)
internal/ui/dialog/models.go+4−1
@@ -69,6 +69,8 @@ const (
69// ModelsID is the identifier for the model selection dialog.
69// ModelsID is the identifier for the model selection dialog.
70const ModelsID = "models"
70const ModelsID = "models"
71
71
72+const defaultModelsDialogMaxWidth = 70
73+
72// Models represents a model selection dialog.
74// Models represents a model selection dialog.
73type Models struct {
75type Models struct {
74 com *common.Common
76 com *common.Common
@@ -240,7 +242,7 @@ func (m *Models) modelTypeRadioView() string {
240// Draw implements [Dialog].
242// Draw implements [Dialog].
241func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
243func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
242 t := m.com.Styles
244 t := m.com.Styles
243− width := max(0, min(defaultDialogMaxWidth, area.Dx()))
245+ width := max(0, min(defaultModelsDialogMaxWidth, area.Dx()))
244 height := max(0, min(defaultDialogHeight, area.Dy()))
246 height := max(0, min(defaultDialogHeight, area.Dy()))
245 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
247 innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
246 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
248 heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
@@ -442,6 +444,7 @@ func (m *Models) setProviderItems() error {
442 // Set model groups in the list.
444 // Set model groups in the list.
443 m.list.SetGroups(groups...)
445 m.list.SetGroups(groups...)
444 m.list.SetSelectedItem(selectedItemID)
446 m.list.SetSelectedItem(selectedItemID)
447+ m.list.ScrollToSelected()
445
448
446 // Update placeholder based on model type
449 // Update placeholder based on model type
447 m.input.Placeholder = m.modelType.Placeholder()
450 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)
57 s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
57 s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
58 s.list.Focus()
58 s.list.Focus()
59 s.list.SetSelected(s.selectedSessionInx)
59 s.list.SetSelected(s.selectedSessionInx)
60− s.list.ScrollToSelected()
61
60
62 s.input = textinput.New()
61 s.input = textinput.New()
63 s.input.SetVirtualCursor(false)
62 s.input.SetVirtualCursor(false)
@@ -153,6 +152,14 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
153 s.list.SetSize(innerWidth, height-heightOffset)
152 s.list.SetSize(innerWidth, height-heightOffset)
154 s.help.SetWidth(innerWidth)
153 s.help.SetWidth(innerWidth)
155
154
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+
156 rc := NewRenderContext(t, width)
163 rc := NewRenderContext(t, width)
157 rc.Title = "Switch Session"
164 rc.Title = "Switch Session"
158 inputView := t.Dialog.InputPrompt.Render(s.input.View())
165 inputView := t.Dialog.InputPrompt.Render(s.input.View())
internal/ui/model/keys.go+2−2
@@ -166,11 +166,11 @@ func DefaultKeyMap() KeyMap {
166 )
166 )
167
167
168 km.Chat.Down = key.NewBinding(
168 km.Chat.Down = key.NewBinding(
169− key.WithKeys("down", "ctrl+j", "ctrl+n", "j"),
169+ key.WithKeys("down", "ctrl+j", "j"),
170 key.WithHelp("↓", "down"),
170 key.WithHelp("↓", "down"),
171 )
171 )
172 km.Chat.Up = key.NewBinding(
172 km.Chat.Up = key.NewBinding(
173− key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
173+ key.WithKeys("up", "ctrl+k", "k"),
174 key.WithHelp("↑", "up"),
174 key.WithHelp("↑", "up"),
175 )
175 )
176 km.Chat.UpDown = key.NewBinding(
176 km.Chat.UpDown = key.NewBinding(
internal/ui/model/lsp.go+8−1
@@ -23,8 +23,14 @@ type LSPInfo struct {
23func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
23func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
24 var lsps []LSPInfo
24 var lsps []LSPInfo
25 t := m.com.Styles
25 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+ }
26
33
27− for _, state := range m.lspStates {
28 client, ok := m.com.App.LSPClients.Get(state.Name)
34 client, ok := m.com.App.LSPClients.Get(state.Name)
29 if !ok {
35 if !ok {
30 continue
36 continue
@@ -39,6 +45,7 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
39
45
40 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
46 lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
41 }
47 }
48+
42 title := t.Subtle.Render("LSPs")
49 title := t.Subtle.Render("LSPs")
43 if isSection {
50 if isSection {
44 title = common.Section(t, title, width)
51 title = common.Section(t, title, width)
internal/ui/model/onboarding.go+19−6
@@ -8,9 +8,12 @@ import (
8 "charm.land/bubbles/v2/key"
8 "charm.land/bubbles/v2/key"
9 tea "charm.land/bubbletea/v2"
9 tea "charm.land/bubbletea/v2"
10 "charm.land/lipgloss/v2"
10 "charm.land/lipgloss/v2"
11+
12+ "github.com/charmbracelet/crush/internal/agent"
11 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/home"
14 "github.com/charmbracelet/crush/internal/home"
13 "github.com/charmbracelet/crush/internal/ui/common"
15 "github.com/charmbracelet/crush/internal/ui/common"
16+ "github.com/charmbracelet/crush/internal/uiutil"
14)
17)
15
18
16// markProjectInitialized marks the current project as initialized in the config.
19// markProjectInitialized marks the current project as initialized in the config.
@@ -44,12 +47,22 @@ func (m *UI) updateInitializeView(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
44
47
45// initializeProject starts project initialization and transitions to the landing view.
48// initializeProject starts project initialization and transitions to the landing view.
46func (m *UI) initializeProject() tea.Cmd {
49func (m *UI) initializeProject() tea.Cmd {
47− // TODO: initialize the project
50+ // clear the session
48− // for now we just go to the landing page
51+ m.newSession()
49− m.state = uiLanding
52+ cfg := m.com.Config()
50− m.focus = uiFocusEditor
53+ var cmds []tea.Cmd
51− // TODO: actually send a message to the agent
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
52− return m.markProjectInitialized
63+ cmds = append(cmds, initialize, m.markProjectInitialized)
64+
65+ return tea.Sequence(cmds...)
53}
66}
54
67
55// skipInitializeProject skips project initialization and transitions to the landing view.
68// 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 {
265 completions: comp,
265 completions: comp,
266 attachments: attachments,
266 attachments: attachments,
267 todoSpinner: todoSpinner,
267 todoSpinner: todoSpinner,
268+ lspStates: make(map[string]app.LSPClientInfo),
269+ mcpStates: make(map[string]mcp.ClientInfo),
268 }
270 }
269
271
270 status := NewStatus(com, ui)
272 status := NewStatus(com, ui)
@@ -1106,6 +1108,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
1106 break
1108 break
1107 }
1109 }
1108 cmds = append(cmds, m.initializeProject())
1110 cmds = append(cmds, m.initializeProject())
1111+ m.dialog.CloseDialog(dialog.CommandsID)
1109
1112
1110 case dialog.ActionSelectModel:
1113 case dialog.ActionSelectModel:
1111 if m.isAgentBusy() {
1114 if m.isAgentBusy() {
@@ -1329,6 +1332,13 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1329 }
1332 }
1330 return true
1333 return true
1331 }
1334 }
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
1332 }
1342 }
1333 return false
1343 return false
1334 }
1344 }
@@ -1430,10 +1440,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1430 }
1440 }
1431 m.newSession()
1441 m.newSession()
1432 case key.Matches(msg, m.keyMap.Tab):
1442 case key.Matches(msg, m.keyMap.Tab):
1443+ if m.state != uiLanding {
1433− m.focus = uiFocusMain
1444+ m.focus = uiFocusMain
1434− m.textarea.Blur()
1445+ m.textarea.Blur()
1435− m.chat.Focus()
1446+ m.chat.Focus()
1436− m.chat.SetSelected(m.chat.Len() - 1)
1447+ m.chat.SetSelected(m.chat.Len() - 1)
1448+ }
1437 case key.Matches(msg, m.keyMap.Editor.OpenEditor):
1449 case key.Matches(msg, m.keyMap.Editor.OpenEditor):
1438 if m.isAgentBusy() {
1450 if m.isAgentBusy() {
1439 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
1451 cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
@@ -1506,6 +1518,16 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
1506 m.focus = uiFocusEditor
1518 m.focus = uiFocusEditor
1507 cmds = append(cmds, m.textarea.Focus())
1519 cmds = append(cmds, m.textarea.Focus())
1508 m.chat.Blur()
1520 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()
1509 case key.Matches(msg, m.keyMap.Chat.Expand):
1531 case key.Matches(msg, m.keyMap.Chat.Expand):
1510 m.chat.ToggleExpandedSelectedItem()
1532 m.chat.ToggleExpandedSelectedItem()
1511 case key.Matches(msg, m.keyMap.Chat.Up):
1533 case key.Matches(msg, m.keyMap.Chat.Up):