@@ -2,7 +2,6 @@ package dialog
22
33import (
44 "os"
5− "strings"
65
76 "charm.land/bubbles/v2/help"
87 "charm.land/bubbles/v2/key"
@@ -13,29 +12,16 @@ import (
1312 "github.com/charmbracelet/crush/internal/config"
1413 "github.com/charmbracelet/crush/internal/ui/common"
1514 "github.com/charmbracelet/crush/internal/ui/list"
16− "github.com/charmbracelet/crush/internal/ui/styles"
1715 uv "github.com/charmbracelet/ultraviolet"
1816)
1917
2018// CommandsID is the identifier for the commands dialog.
2119const CommandsID = "commands"
2220
23−// CommandType represents the type of commands being displayed.
24−type CommandType uint
25−
26−// String returns the string representation of the CommandType.
27−func (c CommandType) String() string { return []string{"System", "User", "MCP"}[c] }
28−
2921const (
3022 sidebarCompactModeBreakpoint = 120
3123)
3224
33−const (
34− SystemCommands CommandType = iota
35− UserCommands
36− MCPPrompts
37−)
38−
3925// Commands represents a dialog that shows available commands.
4026type dockerMCPAvailabilityCheckedMsg struct {
4127 available bool
@@ -48,8 +34,6 @@ type Commands struct {
4834 UpDown,
4935 Next,
5036 Previous,
51− Tab,
52− ShiftTab,
5337 Close key.Binding
5438 }
5539
@@ -57,7 +41,6 @@ type Commands struct {
5741 hasSession bool
5842 hasTodos bool
5943 hasQueue bool
60− selected CommandType
6144
6245 spinner spinner.Model
6346 loading bool
@@ -81,7 +64,6 @@ var _ Dialog = (*Commands)(nil)
8164func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, hasQueue bool, customCommands []commands.CustomCommand, mcpPrompts []commands.MCPPrompt) (*Commands, error) {
8265 c := &Commands{
8366 com: com,
84− selected: SystemCommands,
8567 sessionID: sessionID,
8668 hasSession: hasSession,
8769 hasTodos: hasTodos,
@@ -123,14 +105,6 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
123105 key.WithKeys("up", "ctrl+p"),
124106 key.WithHelp("↑", "previous item"),
125107 )
126− c.keyMap.Tab = key.NewBinding(
127− key.WithKeys("tab"),
128− key.WithHelp("tab", "switch selection"),
129− )
130− c.keyMap.ShiftTab = key.NewBinding(
131− key.WithKeys("shift+tab"),
132− key.WithHelp("shift+tab", "switch selection prev"),
133− )
134108 closeKey := CloseKey
135109 closeKey.SetHelp("esc", "cancel")
136110 c.keyMap.Close = closeKey
@@ -140,7 +114,7 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has
140114 }
141115
142116 // Set initial commands
143− c.setCommandItems(c.selected)
117+ c.setCommandItems()
144118
145119 s := spinner.New()
146120 s.Spinner = spinner.Dot
@@ -161,9 +135,7 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
161135 case dockerMCPAvailabilityCheckedMsg:
162136 c.dockerMCPAvailable = &msg.available
163137 c.dockerMCPCheckInFlight = false
164− if c.selected == SystemCommands {
165− c.setCommandItems(c.selected)
166− }
138+ c.setCommandItems()
167139 return nil
168140 case spinner.TickMsg:
169141 if c.loading {
@@ -197,16 +169,6 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action {
197169 return item.Action()
198170 }
199171 }
200− case key.Matches(msg, c.keyMap.Tab):
201− if len(c.customCommands) > 0 || len(c.mcpPrompts) > 0 {
202− c.selected = c.nextCommandType()
203− c.setCommandItems(c.selected)
204− }
205− case key.Matches(msg, c.keyMap.ShiftTab):
206− if len(c.customCommands) > 0 || len(c.mcpPrompts) > 0 {
207− c.selected = c.previousCommandType()
208− c.setCommandItems(c.selected)
209− }
210172 default:
211173 var cmd tea.Cmd
212174 for _, item := range c.list.FilteredItems() {
@@ -246,43 +208,16 @@ func (c *Commands) Cursor() *tea.Cursor {
246208 return InputCursor(c.com.Styles, c.input.Cursor())
247209}
248210
249−// commandsRadioView generates the command type selector radio buttons.
250−func commandsRadioView(sty *styles.Styles, selected CommandType, hasUserCmds bool, hasMCPPrompts bool) string {
251− if !hasUserCmds && !hasMCPPrompts {
252− return ""
253− }
254−
255− selectedFn := func(t CommandType) string {
256− if t == selected {
257− return sty.Radio.On.Padding(0, 1).Render() + sty.Radio.Label.Render(t.String())
258− }
259− return sty.Radio.Off.Padding(0, 1).Render() + sty.Radio.Label.Render(t.String())
260− }
261−
262− parts := []string{
263− selectedFn(SystemCommands),
264− }
265−
266− if hasUserCmds {
267− parts = append(parts, selectedFn(UserCommands))
268− }
269− if hasMCPPrompts {
270− parts = append(parts, selectedFn(MCPPrompts))
271− }
272−
273− return strings.Join(parts, " ")
274−}
275−
276211// Draw implements [Dialog].
277212func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
278213 t := c.com.Styles
279214 width := max(0, min(defaultDialogMaxWidth, area.Dx()-t.Dialog.View.GetHorizontalBorderSize()))
280215 height := max(0, min(defaultDialogHeight, area.Dy()-t.Dialog.View.GetVerticalBorderSize()))
281− if area.Dx() != c.windowWidth && c.selected == SystemCommands {
216+ if area.Dx() != c.windowWidth {
282217 c.windowWidth = area.Dx()
283− // since some items in the list depend on width (e.g. toggle sidebar command),
284− // we need to reset the command items when width changes
285− c.setCommandItems(c.selected)
218+ // Since some items in the list depend on width (e.g. toggle sidebar
219+ // command), we need to reset the command items when width changes.
220+ c.setCommandItems()
286221 }
287222
288223 innerWidth := width - c.com.Styles.Dialog.View.GetHorizontalFrameSize()
@@ -298,7 +233,6 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
298233
299234 rc := NewRenderContext(t, width)
300235 rc.Title = "Commands"
301− rc.TitleInfo = commandsRadioView(t, c.selected, len(c.customCommands) > 0, len(c.mcpPrompts) > 0)
302236 inputView := t.Dialog.InputPrompt.Render(c.input.View())
303237 rc.AddPart(inputView)
304238 listView := t.Dialog.List.Height(c.list.Height()).Render(c.list.Render())
@@ -319,7 +253,6 @@ func (c *Commands) Draw(scr uv.Screen, area uv.Rectangle) *tea.Cursor {
319253// ShortHelp implements [help.KeyMap].
320254func (c *Commands) ShortHelp() []key.Binding {
321255 return []key.Binding{
322− c.keyMap.Tab,
323256 c.keyMap.UpDown,
324257 c.keyMap.Select,
325258 c.keyMap.Close,
@@ -329,96 +262,50 @@ func (c *Commands) ShortHelp() []key.Binding {
329262// FullHelp implements [help.KeyMap].
330263func (c *Commands) FullHelp() [][]key.Binding {
331264 return [][]key.Binding{
332− {c.keyMap.Select, c.keyMap.Next, c.keyMap.Previous, c.keyMap.Tab},
265+ {c.keyMap.Select, c.keyMap.Next, c.keyMap.Previous},
333266 {c.keyMap.Close},
334267 }
335268}
336269
337−// nextCommandType returns the next command type in the cycle.
338−func (c *Commands) nextCommandType() CommandType {
339− switch c.selected {
340− case SystemCommands:
341− if len(c.customCommands) > 0 {
342− return UserCommands
343− }
344− if len(c.mcpPrompts) > 0 {
345− return MCPPrompts
346− }
347− fallthrough
348− case UserCommands:
349− if len(c.mcpPrompts) > 0 {
350− return MCPPrompts
351− }
352− fallthrough
353− case MCPPrompts:
354− return SystemCommands
355− default:
356− return SystemCommands
270+// setCommandItems sets the command items, combining system, user, and MCP
271+// prompt commands into a single unified list.
272+func (c *Commands) setCommandItems() {
273+ commandItems := []list.FilterableItem{}
274+
275+ // System commands.
276+ for _, cmd := range c.defaultCommands() {
277+ commandItems = append(commandItems, cmd)
357278 }
358−}
359279
360−// previousCommandType returns the previous command type in the cycle.
361−func (c *Commands) previousCommandType() CommandType {
362− switch c.selected {
363− case SystemCommands:
364− if len(c.mcpPrompts) > 0 {
365− return MCPPrompts
366− }
367− if len(c.customCommands) > 0 {
368− return UserCommands
280+ // User (custom) commands.
281+ for _, cmd := range c.customCommands {
282+ var action Action
283+ if cmd.Skill != nil {
284+ action = ActionAttachSkill{ID: cmd.Skill.SkillFilePath, Name: cmd.Skill.Name}
285+ } else {
286+ action = ActionRunCustomCommand{
287+ Content: cmd.Content,
288+ Arguments: cmd.Arguments,
289+ Skill: cmd.Skill,
290+ }
369291 }
370− return SystemCommands
371− case UserCommands:
372− return SystemCommands
373− case MCPPrompts:
374− if len(c.customCommands) > 0 {
375− return UserCommands
292+ item := NewCommandItem(c.com.Styles, "custom_"+cmd.ID, cmd.Name, "", action)
293+ if cmd.Skill != nil {
294+ item = item.WithDescription(cmd.Skill.Description)
376295 }
377− return SystemCommands
378− default:
379− return SystemCommands
296+ commandItems = append(commandItems, item)
380297 }
381−}
382−
383−// setCommandItems sets the command items based on the specified command type.
384−func (c *Commands) setCommandItems(commandType CommandType) {
385− c.selected = commandType
386298
387− commandItems := []list.FilterableItem{}
388− switch c.selected {
389− case SystemCommands:
390− for _, cmd := range c.defaultCommands() {
391− commandItems = append(commandItems, cmd)
392− }
393− case UserCommands:
394− for _, cmd := range c.customCommands {
395− var action Action
396− if cmd.Skill != nil {
397− action = ActionAttachSkill{ID: cmd.Skill.SkillFilePath, Name: cmd.Skill.Name}
398− } else {
399− action = ActionRunCustomCommand{
400− Content: cmd.Content,
401− Arguments: cmd.Arguments,
402− Skill: cmd.Skill,
403− }
404− }
405− item := NewCommandItem(c.com.Styles, "custom_"+cmd.ID, cmd.Name, "", action)
406− if cmd.Skill != nil {
407− item = item.WithDescription(cmd.Skill.Description)
408− }
409− commandItems = append(commandItems, item)
410− }
411− case MCPPrompts:
412− for _, cmd := range c.mcpPrompts {
413− action := ActionRunMCPPrompt{
414− Title: cmd.Title,
415− Description: cmd.Description,
416− PromptID: cmd.PromptID,
417− ClientID: cmd.ClientID,
418− Arguments: cmd.Arguments,
419− }
420− commandItems = append(commandItems, NewCommandItem(c.com.Styles, "mcp_"+cmd.ID, cmd.PromptID, "", action))
299+ // MCP prompts.
300+ for _, cmd := range c.mcpPrompts {
301+ action := ActionRunMCPPrompt{
302+ Title: cmd.Title,
303+ Description: cmd.Description,
304+ PromptID: cmd.PromptID,
305+ ClientID: cmd.ClientID,
306+ Arguments: cmd.Arguments,
421307 }
308+ commandItems = append(commandItems, NewCommandItem(c.com.Styles, "mcp_"+cmd.ID, cmd.PromptID, "", action))
422309 }
423310
424311 c.list.SetItems(commandItems...)
@@ -539,20 +426,16 @@ func (c *Commands) defaultCommands() []*CommandItem {
539426 return commands
540427}
541428
542−// SetCustomCommands sets the custom commands and refreshes the view if user commands are currently displayed.
429+// SetCustomCommands sets the custom commands and refreshes the view.
543430func (c *Commands) SetCustomCommands(customCommands []commands.CustomCommand) {
544431 c.customCommands = customCommands
545− if c.selected == UserCommands {
546− c.setCommandItems(c.selected)
547− }
432+ c.setCommandItems()
548433}
549434
550−// SetMCPPrompts sets the MCP prompts and refreshes the view if MCP prompts are currently displayed.
435+// SetMCPPrompts sets the MCP prompts and refreshes the view.
551436func (c *Commands) SetMCPPrompts(mcpPrompts []commands.MCPPrompt) {
552437 c.mcpPrompts = mcpPrompts
553− if c.selected == MCPPrompts {
554− c.setCommandItems(c.selected)
555− }
438+ c.setCommandItems()
556439}
557440
558441// StartLoading implements [LoadingDialog].