tools.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package dialog
  2. import (
  3. "github.com/charmbracelet/bubbles/key"
  4. tea "github.com/charmbracelet/bubbletea"
  5. "github.com/charmbracelet/lipgloss"
  6. utilComponents "github.com/sst/opencode/internal/tui/components/util"
  7. "github.com/sst/opencode/internal/tui/layout"
  8. "github.com/sst/opencode/internal/tui/styles"
  9. "github.com/sst/opencode/internal/tui/theme"
  10. )
  11. const (
  12. maxToolsDialogWidth = 60
  13. maxVisibleTools = 15
  14. )
  15. // ToolsDialog interface for the tools list dialog
  16. type ToolsDialog interface {
  17. tea.Model
  18. layout.Bindings
  19. SetTools(tools []string)
  20. }
  21. // ShowToolsDialogMsg is sent to show the tools dialog
  22. type ShowToolsDialogMsg struct {
  23. Show bool
  24. }
  25. // CloseToolsDialogMsg is sent when the tools dialog is closed
  26. type CloseToolsDialogMsg struct{}
  27. type toolItem struct {
  28. name string
  29. }
  30. func (t toolItem) Render(selected bool, width int) string {
  31. th := theme.CurrentTheme()
  32. baseStyle := styles.BaseStyle().
  33. Width(width).
  34. Background(th.Background())
  35. if selected {
  36. baseStyle = baseStyle.
  37. Background(th.Primary()).
  38. Foreground(th.Background()).
  39. Bold(true)
  40. } else {
  41. baseStyle = baseStyle.
  42. Foreground(th.Text())
  43. }
  44. return baseStyle.Render(t.name)
  45. }
  46. type toolsDialogCmp struct {
  47. tools []toolItem
  48. width int
  49. height int
  50. list utilComponents.SimpleList[toolItem]
  51. }
  52. type toolsKeyMap struct {
  53. Up key.Binding
  54. Down key.Binding
  55. Escape key.Binding
  56. J key.Binding
  57. K key.Binding
  58. }
  59. var toolsKeys = toolsKeyMap{
  60. Up: key.NewBinding(
  61. key.WithKeys("up"),
  62. key.WithHelp("↑", "previous tool"),
  63. ),
  64. Down: key.NewBinding(
  65. key.WithKeys("down"),
  66. key.WithHelp("↓", "next tool"),
  67. ),
  68. Escape: key.NewBinding(
  69. key.WithKeys("esc"),
  70. key.WithHelp("esc", "close"),
  71. ),
  72. J: key.NewBinding(
  73. key.WithKeys("j"),
  74. key.WithHelp("j", "next tool"),
  75. ),
  76. K: key.NewBinding(
  77. key.WithKeys("k"),
  78. key.WithHelp("k", "previous tool"),
  79. ),
  80. }
  81. func (m *toolsDialogCmp) Init() tea.Cmd {
  82. return nil
  83. }
  84. func (m *toolsDialogCmp) SetTools(tools []string) {
  85. var toolItems []toolItem
  86. for _, name := range tools {
  87. toolItems = append(toolItems, toolItem{name: name})
  88. }
  89. m.tools = toolItems
  90. m.list.SetItems(toolItems)
  91. }
  92. func (m *toolsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  93. switch msg := msg.(type) {
  94. case tea.KeyMsg:
  95. switch {
  96. case key.Matches(msg, toolsKeys.Escape):
  97. return m, func() tea.Msg { return CloseToolsDialogMsg{} }
  98. // Pass other key messages to the list component
  99. default:
  100. var cmd tea.Cmd
  101. listModel, cmd := m.list.Update(msg)
  102. m.list = listModel.(utilComponents.SimpleList[toolItem])
  103. return m, cmd
  104. }
  105. case tea.WindowSizeMsg:
  106. m.width = msg.Width
  107. m.height = msg.Height
  108. }
  109. // For non-key messages
  110. var cmd tea.Cmd
  111. listModel, cmd := m.list.Update(msg)
  112. m.list = listModel.(utilComponents.SimpleList[toolItem])
  113. return m, cmd
  114. }
  115. func (m *toolsDialogCmp) View() string {
  116. t := theme.CurrentTheme()
  117. baseStyle := styles.BaseStyle().Background(t.Background())
  118. title := baseStyle.
  119. Foreground(t.Primary()).
  120. Bold(true).
  121. Width(maxToolsDialogWidth).
  122. Padding(0, 0, 1).
  123. Render("Available Tools")
  124. // Calculate dialog width based on content
  125. dialogWidth := min(maxToolsDialogWidth, m.width/2)
  126. m.list.SetMaxWidth(dialogWidth)
  127. content := lipgloss.JoinVertical(
  128. lipgloss.Left,
  129. title,
  130. m.list.View(),
  131. )
  132. return baseStyle.Padding(1, 2).
  133. Border(lipgloss.RoundedBorder()).
  134. BorderBackground(t.Background()).
  135. BorderForeground(t.TextMuted()).
  136. Background(t.Background()).
  137. Width(lipgloss.Width(content) + 4).
  138. Render(content)
  139. }
  140. func (m *toolsDialogCmp) BindingKeys() []key.Binding {
  141. return layout.KeyMapToSlice(toolsKeys)
  142. }
  143. func NewToolsDialogCmp() ToolsDialog {
  144. list := utilComponents.NewSimpleList[toolItem](
  145. []toolItem{},
  146. maxVisibleTools,
  147. "No tools available",
  148. true,
  149. )
  150. return &toolsDialogCmp{
  151. list: list,
  152. }
  153. }