agents.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package dialog
  2. import (
  3. "fmt"
  4. "sort"
  5. "github.com/charmbracelet/bubbles/v2/key"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/lithammer/fuzzysearch/fuzzy"
  8. "github.com/sst/opencode-sdk-go"
  9. "github.com/sst/opencode/internal/app"
  10. "github.com/sst/opencode/internal/components/list"
  11. "github.com/sst/opencode/internal/components/modal"
  12. "github.com/sst/opencode/internal/layout"
  13. "github.com/sst/opencode/internal/styles"
  14. "github.com/sst/opencode/internal/theme"
  15. "github.com/sst/opencode/internal/util"
  16. )
  17. const (
  18. numVisibleAgents = 10
  19. minAgentDialogWidth = 54
  20. maxAgentDialogWidth = 108
  21. maxDescriptionLength = 80
  22. )
  23. // AgentDialog interface for the agent selection dialog
  24. type AgentDialog interface {
  25. layout.Modal
  26. }
  27. type agentDialog struct {
  28. app *app.App
  29. allAgents []opencode.Agent
  30. width int
  31. height int
  32. modal *modal.Modal
  33. searchDialog *SearchDialog
  34. dialogWidth int
  35. }
  36. // agentItem is a custom list item for agent selections
  37. type agentItem struct {
  38. agent opencode.Agent
  39. }
  40. func (a agentItem) Render(
  41. selected bool,
  42. width int,
  43. baseStyle styles.Style,
  44. ) string {
  45. t := theme.CurrentTheme()
  46. itemStyle := baseStyle.
  47. Background(t.BackgroundPanel()).
  48. Foreground(t.Text())
  49. if selected {
  50. itemStyle = itemStyle.Foreground(t.Primary())
  51. }
  52. descStyle := baseStyle.
  53. Foreground(t.TextMuted()).
  54. Background(t.BackgroundPanel())
  55. // Calculate available width (accounting for padding and margins)
  56. availableWidth := width - 2 // Account for left padding
  57. agentName := a.agent.Name
  58. description := a.agent.Description
  59. if description == "" {
  60. description = fmt.Sprintf("(%s)", a.agent.Mode)
  61. }
  62. separator := " - "
  63. // Calculate how much space we have for the description
  64. nameAndSeparatorLength := len(agentName) + len(separator)
  65. descriptionMaxLength := availableWidth - nameAndSeparatorLength
  66. // Truncate description if it's too long
  67. if len(description) > descriptionMaxLength && descriptionMaxLength > 3 {
  68. description = description[:descriptionMaxLength-3] + "..."
  69. }
  70. namePart := itemStyle.Render(agentName)
  71. descPart := descStyle.Render(separator + description)
  72. combinedText := namePart + descPart
  73. return baseStyle.
  74. Background(t.BackgroundPanel()).
  75. PaddingLeft(1).
  76. Width(width).
  77. Render(combinedText)
  78. }
  79. func (a agentItem) Selectable() bool {
  80. // All agents in the dialog are selectable (subagents are filtered out)
  81. return true
  82. }
  83. type agentKeyMap struct {
  84. Enter key.Binding
  85. Escape key.Binding
  86. }
  87. var agentKeys = agentKeyMap{
  88. Enter: key.NewBinding(
  89. key.WithKeys("enter"),
  90. key.WithHelp("enter", "select agent"),
  91. ),
  92. Escape: key.NewBinding(
  93. key.WithKeys("esc"),
  94. key.WithHelp("esc", "close"),
  95. ),
  96. }
  97. func (a *agentDialog) Init() tea.Cmd {
  98. a.setupAllAgents()
  99. return a.searchDialog.Init()
  100. }
  101. func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  102. switch msg := msg.(type) {
  103. case SearchSelectionMsg:
  104. // Handle selection from search dialog
  105. if item, ok := msg.Item.(agentItem); ok {
  106. return a, tea.Sequence(
  107. util.CmdHandler(modal.CloseModalMsg{}),
  108. util.CmdHandler(
  109. app.AgentSelectedMsg{
  110. Agent: item.agent,
  111. }),
  112. )
  113. }
  114. return a, util.CmdHandler(modal.CloseModalMsg{})
  115. case SearchCancelledMsg:
  116. return a, util.CmdHandler(modal.CloseModalMsg{})
  117. case SearchQueryChangedMsg:
  118. // Update the list based on search query
  119. items := a.buildDisplayList(msg.Query)
  120. a.searchDialog.SetItems(items)
  121. return a, nil
  122. case tea.WindowSizeMsg:
  123. a.width = msg.Width
  124. a.height = msg.Height
  125. a.searchDialog.SetWidth(a.dialogWidth)
  126. a.searchDialog.SetHeight(msg.Height)
  127. }
  128. updatedDialog, cmd := a.searchDialog.Update(msg)
  129. a.searchDialog = updatedDialog.(*SearchDialog)
  130. return a, cmd
  131. }
  132. func (a *agentDialog) View() string {
  133. return a.searchDialog.View()
  134. }
  135. func (a *agentDialog) calculateOptimalWidth(agents []opencode.Agent) int {
  136. maxWidth := minAgentDialogWidth
  137. for _, agent := range agents {
  138. // Calculate the width needed for this item: "AgentName - Description"
  139. itemWidth := len(agent.Name)
  140. if agent.Description != "" {
  141. itemWidth += len(agent.Description) + 3 // " - "
  142. } else {
  143. itemWidth += len(string(agent.Mode)) + 3 // " (mode)"
  144. }
  145. if itemWidth > maxWidth {
  146. maxWidth = itemWidth
  147. }
  148. }
  149. maxWidth = min(maxWidth, maxAgentDialogWidth)
  150. return maxWidth
  151. }
  152. func (a *agentDialog) setupAllAgents() {
  153. // Get agents from the app, filtering out subagents
  154. a.allAgents = []opencode.Agent{}
  155. for _, agent := range a.app.Agents {
  156. if agent.Mode != "subagent" {
  157. a.allAgents = append(a.allAgents, agent)
  158. }
  159. }
  160. a.sortAgents()
  161. // Calculate optimal width based on all agents
  162. a.dialogWidth = a.calculateOptimalWidth(a.allAgents)
  163. // Ensure minimum width to prevent textinput issues
  164. a.dialogWidth = max(a.dialogWidth, minAgentDialogWidth)
  165. a.searchDialog = NewSearchDialog("Search agents...", numVisibleAgents)
  166. a.searchDialog.SetWidth(a.dialogWidth)
  167. items := a.buildDisplayList("")
  168. a.searchDialog.SetItems(items)
  169. }
  170. func (a *agentDialog) sortAgents() {
  171. sort.Slice(a.allAgents, func(i, j int) bool {
  172. agentA := a.allAgents[i]
  173. agentB := a.allAgents[j]
  174. // Current agent goes first
  175. if agentA.Name == a.app.Agent().Name {
  176. return true
  177. }
  178. if agentB.Name == a.app.Agent().Name {
  179. return false
  180. }
  181. // Alphabetical order for all other agents
  182. return agentA.Name < agentB.Name
  183. })
  184. }
  185. func (a *agentDialog) buildDisplayList(query string) []list.Item {
  186. if query != "" {
  187. return a.buildSearchResults(query)
  188. }
  189. return a.buildGroupedResults()
  190. }
  191. func (a *agentDialog) buildSearchResults(query string) []list.Item {
  192. agentNames := []string{}
  193. agentMap := make(map[string]opencode.Agent)
  194. for _, agent := range a.allAgents {
  195. // Search by name
  196. searchStr := agent.Name
  197. agentNames = append(agentNames, searchStr)
  198. agentMap[searchStr] = agent
  199. // Search by description if available
  200. if agent.Description != "" {
  201. searchStr = fmt.Sprintf("%s %s", agent.Name, agent.Description)
  202. agentNames = append(agentNames, searchStr)
  203. agentMap[searchStr] = agent
  204. }
  205. }
  206. matches := fuzzy.RankFindFold(query, agentNames)
  207. sort.Sort(matches)
  208. items := []list.Item{}
  209. seenAgents := make(map[string]bool)
  210. for _, match := range matches {
  211. agent := agentMap[match.Target]
  212. // Create a unique key to avoid duplicates
  213. key := agent.Name
  214. if seenAgents[key] {
  215. continue
  216. }
  217. seenAgents[key] = true
  218. items = append(items, agentItem{agent: agent})
  219. }
  220. return items
  221. }
  222. func (a *agentDialog) buildGroupedResults() []list.Item {
  223. var items []list.Item
  224. items = append(items, list.HeaderItem("Agents"))
  225. // Add all agents (subagents are already filtered out)
  226. for _, agent := range a.allAgents {
  227. items = append(items, agentItem{agent: agent})
  228. }
  229. return items
  230. }
  231. func (a *agentDialog) Render(background string) string {
  232. return a.modal.Render(a.View(), background)
  233. }
  234. func (s *agentDialog) Close() tea.Cmd {
  235. return nil
  236. }
  237. func NewAgentDialog(app *app.App) AgentDialog {
  238. dialog := &agentDialog{
  239. app: app,
  240. }
  241. dialog.setupAllAgents()
  242. dialog.modal = modal.New(
  243. modal.WithTitle("Select Agent"),
  244. modal.WithMaxWidth(dialog.dialogWidth+4),
  245. )
  246. return dialog
  247. }