diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 943c3efc41b33ea9f261b4ffc7256b6f544beff9..fd6662c1961c7f5f8aa6289b38e89b0aa9dc521a 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -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 { diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index 9039a2457d3c86ba21886deac4137970f861fd59..66d924a28a64a7ad9dff3081878290b3f74cc230 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -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) diff --git a/internal/ui/dialog/models.go b/internal/ui/dialog/models.go index 77aeab22380f89455f83760428fac128fd4fc28b..31c0f0a886b72d409696f1a3a6c4305488ed9539 100644 --- a/internal/ui/dialog/models.go +++ b/internal/ui/dialog/models.go @@ -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() diff --git a/internal/ui/dialog/sessions.go b/internal/ui/dialog/sessions.go index a70d13ce58fed2ddf1b292d30e405362cf093569..9f7023a44e005c4d6ab735206824519e5b91e5bf 100644 --- a/internal/ui/dialog/sessions.go +++ b/internal/ui/dialog/sessions.go @@ -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()) diff --git a/internal/ui/model/keys.go b/internal/ui/model/keys.go index 053c30aaa1b51b1fd04bc8a3e754460519336359..6e21e4dee0dbae1dffc124066b01185c7ebc9d3a 100644 --- a/internal/ui/model/keys.go +++ b/internal/ui/model/keys.go @@ -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( diff --git a/internal/ui/model/lsp.go b/internal/ui/model/lsp.go index 61e9f75d478ef51daee465ca7eeca109acd6c64b..de33142d51c720265ad84d317b83f5a997f69fac 100644 --- a/internal/ui/model/lsp.go +++ b/internal/ui/model/lsp.go @@ -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) diff --git a/internal/ui/model/onboarding.go b/internal/ui/model/onboarding.go index 1b922282ae78bf0a89004abfff6098ec3240ff94..d18469ee822460e60544a304afebb37dac7fa0d9 100644 --- a/internal/ui/model/onboarding.go +++ b/internal/ui/model/onboarding.go @@ -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. diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 1c3320225b190d028a83c902f60c99c614023f3d..fa1ffed72bd02a38a00b187cbf749cc2c74af2b1 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -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):