Detailed changes
@@ -117,6 +117,11 @@ func (c *coordinator) Run(ctx context.Context, sessionID string, prompt string,
return nil, err
}
+ // refresh models before each run
+ if err := c.UpdateModels(ctx); err != nil {
+ return nil, fmt.Errorf("failed to update models: %w", err)
+ }
+
model := c.currentAgent.Model()
maxTokens := model.CatwalkCfg.DefaultMaxTokens
if model.ModelCfg.MaxTokens != 0 {
@@ -28,7 +28,10 @@ type CommandType uint
// String returns the string representation of the CommandType.
func (c CommandType) String() string { return []string{"System", "User", "MCP"}[c] }
-const sidebarCompactModeBreakpoint = 120
+const (
+ sidebarCompactModeBreakpoint = 120
+ defaultCommandsDialogMaxWidth = 70
+)
const (
SystemCommands CommandType = iota
@@ -238,7 +241,7 @@ func commandsRadioView(sty *styles.Styles, selected CommandType, hasUserCmds boo
// Draw implements [Dialog].
func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
t := c.com.Styles
- width := max(0, min(defaultDialogMaxWidth, area.Dx()))
+ width := max(0, min(defaultCommandsDialogMaxWidth, area.Dx()))
height := max(0, min(defaultDialogHeight, area.Dy()))
if area.Dx() != c.windowWidth && c.selected == SystemCommands {
c.windowWidth = area.Dx()
@@ -254,7 +257,9 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
t.Dialog.View.GetVerticalFrameSize()
c.input.SetWidth(innerWidth - t.Dialog.InputPrompt.GetHorizontalFrameSize() - 1) // (1) cursor padding
- c.list.SetSize(innerWidth, height-heightOffset)
+
+ listHeight := min(height-heightOffset, c.list.Len())
+ c.list.SetSize(innerWidth, listHeight)
c.help.SetWidth(innerWidth)
rc := NewRenderContext(t, width)
@@ -69,6 +69,8 @@ const (
// ModelsID is the identifier for the model selection dialog.
const ModelsID = "models"
+const defaultModelsDialogMaxWidth = 70
+
// Models represents a model selection dialog.
type Models struct {
com *common.Common
@@ -240,7 +242,7 @@ func (m *Models) modelTypeRadioView() string {
// Draw implements [Dialog].
func (m *Models) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
t := m.com.Styles
- width := max(0, min(defaultDialogMaxWidth, area.Dx()))
+ width := max(0, min(defaultModelsDialogMaxWidth, area.Dx()))
height := max(0, min(defaultDialogHeight, area.Dy()))
innerWidth := width - t.Dialog.View.GetHorizontalFrameSize()
heightOffset := t.Dialog.Title.GetVerticalFrameSize() + titleContentHeight +
@@ -442,6 +444,7 @@ func (m *Models) setProviderItems() error {
// Set model groups in the list.
m.list.SetGroups(groups...)
m.list.SetSelectedItem(selectedItemID)
+ m.list.ScrollToSelected()
// Update placeholder based on model type
m.input.Placeholder = m.modelType.Placeholder()
@@ -57,7 +57,6 @@ func NewSessions(com *common.Common, selectedSessionID string) (*Session, error)
s.list = list.NewFilterableList(sessionItems(com.Styles, sessions...)...)
s.list.Focus()
s.list.SetSelected(s.selectedSessionInx)
- s.list.ScrollToSelected()
s.input = textinput.New()
s.input.SetVirtualCursor(false)
@@ -153,6 +152,14 @@ func (s *Session) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
s.list.SetSize(innerWidth, height-heightOffset)
s.help.SetWidth(innerWidth)
+ // This makes it so we do not scroll the list if we don't have to
+ start, end := s.list.VisibleItemIndices()
+
+ // if selected index is outside visible range, scroll to it
+ if s.selectedSessionInx < start || s.selectedSessionInx > end {
+ s.list.ScrollToSelected()
+ }
+
rc := NewRenderContext(t, width)
rc.Title = "Switch Session"
inputView := t.Dialog.InputPrompt.Render(s.input.View())
@@ -166,11 +166,11 @@ func DefaultKeyMap() KeyMap {
)
km.Chat.Down = key.NewBinding(
- key.WithKeys("down", "ctrl+j", "ctrl+n", "j"),
+ key.WithKeys("down", "ctrl+j", "j"),
key.WithHelp("↓", "down"),
)
km.Chat.Up = key.NewBinding(
- key.WithKeys("up", "ctrl+k", "ctrl+p", "k"),
+ key.WithKeys("up", "ctrl+k", "k"),
key.WithHelp("↑", "up"),
)
km.Chat.UpDown = key.NewBinding(
@@ -23,8 +23,14 @@ type LSPInfo struct {
func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
var lsps []LSPInfo
t := m.com.Styles
+ lspConfigs := m.com.Config().LSP.Sorted()
+
+ for _, cfg := range lspConfigs {
+ state, ok := m.lspStates[cfg.Name]
+ if !ok {
+ continue
+ }
- for _, state := range m.lspStates {
client, ok := m.com.App.LSPClients.Get(state.Name)
if !ok {
continue
@@ -39,6 +45,7 @@ func (m *UI) lspInfo(width, maxItems int, isSection bool) string {
lsps = append(lsps, LSPInfo{LSPClientInfo: state, Diagnostics: lspErrs})
}
+
title := t.Subtle.Render("LSPs")
if isSection {
title = common.Section(t, title, width)
@@ -8,9 +8,12 @@ import (
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
+
+ "github.com/charmbracelet/crush/internal/agent"
"github.com/charmbracelet/crush/internal/config"
"github.com/charmbracelet/crush/internal/home"
"github.com/charmbracelet/crush/internal/ui/common"
+ "github.com/charmbracelet/crush/internal/uiutil"
)
// markProjectInitialized marks the current project as initialized in the config.
@@ -44,12 +47,22 @@ func (m *UI) updateInitializeView(msg tea.KeyPressMsg) (cmds []tea.Cmd) {
// initializeProject starts project initialization and transitions to the landing view.
func (m *UI) initializeProject() tea.Cmd {
- // TODO: initialize the project
- // for now we just go to the landing page
- m.state = uiLanding
- m.focus = uiFocusEditor
- // TODO: actually send a message to the agent
- return m.markProjectInitialized
+ // clear the session
+ m.newSession()
+ cfg := m.com.Config()
+ var cmds []tea.Cmd
+
+ initialize := func() tea.Msg {
+ initPrompt, err := agent.InitializePrompt(*cfg)
+ if err != nil {
+ return uiutil.InfoMsg{Type: uiutil.InfoTypeError, Msg: err.Error()}
+ }
+ return sendMessageMsg{Content: initPrompt}
+ }
+ // Mark the project as initialized
+ cmds = append(cmds, initialize, m.markProjectInitialized)
+
+ return tea.Sequence(cmds...)
}
// skipInitializeProject skips project initialization and transitions to the landing view.
@@ -265,6 +265,8 @@ func New(com *common.Common) *UI {
completions: comp,
attachments: attachments,
todoSpinner: todoSpinner,
+ lspStates: make(map[string]app.LSPClientInfo),
+ mcpStates: make(map[string]mcp.ClientInfo),
}
status := NewStatus(com, ui)
@@ -1106,6 +1108,7 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd {
break
}
cmds = append(cmds, m.initializeProject())
+ m.dialog.CloseDialog(dialog.CommandsID)
case dialog.ActionSelectModel:
if m.isAgentBusy() {
@@ -1329,6 +1332,13 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
}
return true
}
+ case key.Matches(msg, m.keyMap.Suspend):
+ if m.isAgentBusy() {
+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait..."))
+ return true
+ }
+ cmds = append(cmds, tea.Suspend)
+ return true
}
return false
}
@@ -1430,10 +1440,12 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
}
m.newSession()
case key.Matches(msg, m.keyMap.Tab):
- m.focus = uiFocusMain
- m.textarea.Blur()
- m.chat.Focus()
- m.chat.SetSelected(m.chat.Len() - 1)
+ if m.state != uiLanding {
+ m.focus = uiFocusMain
+ m.textarea.Blur()
+ m.chat.Focus()
+ m.chat.SetSelected(m.chat.Len() - 1)
+ }
case key.Matches(msg, m.keyMap.Editor.OpenEditor):
if m.isAgentBusy() {
cmds = append(cmds, uiutil.ReportWarn("Agent is working, please wait..."))
@@ -1506,6 +1518,16 @@ func (m *UI) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
m.focus = uiFocusEditor
cmds = append(cmds, m.textarea.Focus())
m.chat.Blur()
+ case key.Matches(msg, m.keyMap.Chat.NewSession):
+ if !m.hasSession() {
+ break
+ }
+ if m.isAgentBusy() {
+ cmds = append(cmds, uiutil.ReportWarn("Agent is busy, please wait before starting a new session..."))
+ break
+ }
+ m.focus = uiFocusEditor
+ m.newSession()
case key.Matches(msg, m.keyMap.Chat.Expand):
m.chat.ToggleExpandedSelectedItem()
case key.Matches(msg, m.keyMap.Chat.Up):