From e753d09af1e9279302388f7d4a15a7e7135b4396 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 11:51:25 +0100 Subject: [PATCH 1/8] chore: do not scroll sessions if not neccessary this makes it so we only scroll if the selected item is not in the view, this was driving me nuts that it was always putting my last session at the top. --- internal/ui/dialog/sessions.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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()) From 1b61cd2b80d2aebe3f7ced80d944639a1ead6345 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 11:51:38 +0100 Subject: [PATCH 2/8] chore: change the dialog sizes a bit --- internal/ui/dialog/commands.go | 11 ++++++++--- internal/ui/dialog/models.go | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) 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() From 4e66b9c985cfa478dd7d75dcc8eb9bf65077f7d1 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 11:51:53 +0100 Subject: [PATCH 3/8] refactor(ui): enable initialize --- internal/ui/model/onboarding.go | 25 +++++++++++++++++++------ internal/ui/model/ui.go | 1 + 2 files changed, 20 insertions(+), 6 deletions(-) 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..0290ab3d44734b1eacc8ab31b244866086ae1f85 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1106,6 +1106,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() { From f002e6f855752dfa7fc197edbfaa84213771e675 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 12:11:16 +0100 Subject: [PATCH 4/8] fix: lsp sort --- internal/ui/model/lsp.go | 9 ++++++++- internal/ui/model/ui.go | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) 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/ui.go b/internal/ui/model/ui.go index 0290ab3d44734b1eacc8ab31b244866086ae1f85..01165860f85d85bc9d26a0554c4861e4063b8cb7 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) From 891b5353207386f5b3753380dbb41105d2f132bf Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 15:19:07 +0100 Subject: [PATCH 5/8] fix: tab to chat only when in chat --- internal/ui/model/ui.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 01165860f85d85bc9d26a0554c4861e4063b8cb7..94fdfe2a2d550ddbce4b12573b64eceedd33d93a 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1433,10 +1433,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...")) From 698b7c8049bd0bb75fccc47c11d472cb500b16ba Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 15:25:17 +0100 Subject: [PATCH 6/8] fix: handle new session when focused on the list --- internal/ui/model/keys.go | 4 ++-- internal/ui/model/ui.go | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) 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/ui.go b/internal/ui/model/ui.go index 94fdfe2a2d550ddbce4b12573b64eceedd33d93a..634b410ef74d5eb00e9d6e55dd1a87cc850c5d86 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1511,6 +1511,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): From 4cf6af059742f4ed8ba58e1e24ca15bfb0f8aea5 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 15:28:49 +0100 Subject: [PATCH 7/8] fix: make sure we have a fresh model/tools on each call --- internal/agent/coordinator.go | 5 +++++ 1 file changed, 5 insertions(+) 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 { From 1fad83be4a3fb73b18047e00a68af5e3b29e3322 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 22 Jan 2026 15:35:38 +0100 Subject: [PATCH 8/8] fix: add back suspend --- internal/ui/model/ui.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 634b410ef74d5eb00e9d6e55dd1a87cc850c5d86..fa1ffed72bd02a38a00b187cbf749cc2c74af2b1 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1332,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 }