diff options
| author | Aiden Cline <[email protected]> | 2025-08-11 00:46:38 -0500 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-11 01:46:38 -0400 |
| commit | 62b8c7aee0cb4da28b3dbb99ed1aed4be4360127 (patch) | |
| tree | d96030970a95c5390e430e9e4680185e8fab5369 /packages/tui/internal/components | |
| parent | 6145dfcca0b6c611abef992610fd57370f9e3886 (diff) | |
| download | opencode-62b8c7aee0cb4da28b3dbb99ed1aed4be4360127.tar.gz opencode-62b8c7aee0cb4da28b3dbb99ed1aed4be4360127.zip | |
feat (tui): agents dialog (#1802)
Diffstat (limited to 'packages/tui/internal/components')
| -rw-r--r-- | packages/tui/internal/components/dialog/agents.go | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/packages/tui/internal/components/dialog/agents.go b/packages/tui/internal/components/dialog/agents.go new file mode 100644 index 000000000..49e3e025b --- /dev/null +++ b/packages/tui/internal/components/dialog/agents.go @@ -0,0 +1,305 @@ +package dialog + +import ( + "fmt" + "sort" + + "github.com/charmbracelet/bubbles/v2/key" + tea "github.com/charmbracelet/bubbletea/v2" + "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/components/list" + "github.com/sst/opencode/internal/components/modal" + "github.com/sst/opencode/internal/layout" + "github.com/sst/opencode/internal/styles" + "github.com/sst/opencode/internal/theme" + "github.com/sst/opencode/internal/util" +) + +const ( + numVisibleAgents = 10 + minAgentDialogWidth = 54 + maxAgentDialogWidth = 108 + maxDescriptionLength = 80 +) + +// AgentDialog interface for the agent selection dialog +type AgentDialog interface { + layout.Modal +} + +type agentDialog struct { + app *app.App + allAgents []opencode.Agent + width int + height int + modal *modal.Modal + searchDialog *SearchDialog + dialogWidth int +} + +// agentItem is a custom list item for agent selections +type agentItem struct { + agent opencode.Agent +} + +func (a agentItem) Render( + selected bool, + width int, + baseStyle styles.Style, +) string { + t := theme.CurrentTheme() + + itemStyle := baseStyle. + Background(t.BackgroundPanel()). + Foreground(t.Text()) + + if selected { + itemStyle = itemStyle.Foreground(t.Primary()) + } + + descStyle := baseStyle. + Foreground(t.TextMuted()). + Background(t.BackgroundPanel()) + + // Calculate available width (accounting for padding and margins) + availableWidth := width - 2 // Account for left padding + + agentName := a.agent.Name + description := a.agent.Description + if description == "" { + description = fmt.Sprintf("(%s)", a.agent.Mode) + } + + separator := " - " + + // Calculate how much space we have for the description + nameAndSeparatorLength := len(agentName) + len(separator) + descriptionMaxLength := availableWidth - nameAndSeparatorLength + + // Truncate description if it's too long + if len(description) > descriptionMaxLength && descriptionMaxLength > 3 { + description = description[:descriptionMaxLength-3] + "..." + } + + namePart := itemStyle.Render(agentName) + descPart := descStyle.Render(separator + description) + combinedText := namePart + descPart + + return baseStyle. + Background(t.BackgroundPanel()). + PaddingLeft(1). + Width(width). + Render(combinedText) +} + +func (a agentItem) Selectable() bool { + // All agents in the dialog are selectable (subagents are filtered out) + return true +} + +type agentKeyMap struct { + Enter key.Binding + Escape key.Binding +} + +var agentKeys = agentKeyMap{ + Enter: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "select agent"), + ), + Escape: key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "close"), + ), +} + +func (a *agentDialog) Init() tea.Cmd { + a.setupAllAgents() + return a.searchDialog.Init() +} + +func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case SearchSelectionMsg: + // Handle selection from search dialog + if item, ok := msg.Item.(agentItem); ok { + return a, tea.Sequence( + util.CmdHandler(modal.CloseModalMsg{}), + util.CmdHandler( + app.AgentSelectedMsg{ + Agent: item.agent, + }), + ) + } + return a, util.CmdHandler(modal.CloseModalMsg{}) + case SearchCancelledMsg: + return a, util.CmdHandler(modal.CloseModalMsg{}) + + case SearchQueryChangedMsg: + // Update the list based on search query + items := a.buildDisplayList(msg.Query) + a.searchDialog.SetItems(items) + return a, nil + + case tea.WindowSizeMsg: + a.width = msg.Width + a.height = msg.Height + a.searchDialog.SetWidth(a.dialogWidth) + a.searchDialog.SetHeight(msg.Height) + } + + updatedDialog, cmd := a.searchDialog.Update(msg) + a.searchDialog = updatedDialog.(*SearchDialog) + return a, cmd +} + +func (a *agentDialog) View() string { + return a.searchDialog.View() +} + +func (a *agentDialog) calculateOptimalWidth(agents []opencode.Agent) int { + maxWidth := minAgentDialogWidth + + for _, agent := range agents { + // Calculate the width needed for this item: "AgentName - Description" + itemWidth := len(agent.Name) + if agent.Description != "" { + itemWidth += len(agent.Description) + 3 // " - " + } else { + itemWidth += len(string(agent.Mode)) + 3 // " (mode)" + } + + if itemWidth > maxWidth { + maxWidth = itemWidth + } + } + + maxWidth = min(maxWidth, maxAgentDialogWidth) + + return maxWidth +} + +func (a *agentDialog) setupAllAgents() { + // Get agents from the app, filtering out subagents + a.allAgents = []opencode.Agent{} + for _, agent := range a.app.Agents { + if agent.Mode != "subagent" { + a.allAgents = append(a.allAgents, agent) + } + } + + a.sortAgents() + + // Calculate optimal width based on all agents + a.dialogWidth = a.calculateOptimalWidth(a.allAgents) + + // Ensure minimum width to prevent textinput issues + a.dialogWidth = max(a.dialogWidth, minAgentDialogWidth) + + a.searchDialog = NewSearchDialog("Search agents...", numVisibleAgents) + a.searchDialog.SetWidth(a.dialogWidth) + + items := a.buildDisplayList("") + a.searchDialog.SetItems(items) +} + +func (a *agentDialog) sortAgents() { + sort.Slice(a.allAgents, func(i, j int) bool { + agentA := a.allAgents[i] + agentB := a.allAgents[j] + + // Current agent goes first + if agentA.Name == a.app.Agent().Name { + return true + } + if agentB.Name == a.app.Agent().Name { + return false + } + + // Alphabetical order for all other agents + return agentA.Name < agentB.Name + }) +} + +func (a *agentDialog) buildDisplayList(query string) []list.Item { + if query != "" { + return a.buildSearchResults(query) + } + return a.buildGroupedResults() +} + +func (a *agentDialog) buildSearchResults(query string) []list.Item { + agentNames := []string{} + agentMap := make(map[string]opencode.Agent) + + for _, agent := range a.allAgents { + // Search by name + searchStr := agent.Name + agentNames = append(agentNames, searchStr) + agentMap[searchStr] = agent + + // Search by description if available + if agent.Description != "" { + searchStr = fmt.Sprintf("%s %s", agent.Name, agent.Description) + agentNames = append(agentNames, searchStr) + agentMap[searchStr] = agent + } + } + + matches := fuzzy.RankFindFold(query, agentNames) + sort.Sort(matches) + + items := []list.Item{} + seenAgents := make(map[string]bool) + + for _, match := range matches { + agent := agentMap[match.Target] + // Create a unique key to avoid duplicates + key := agent.Name + if seenAgents[key] { + continue + } + seenAgents[key] = true + items = append(items, agentItem{agent: agent}) + } + + return items +} + +func (a *agentDialog) buildGroupedResults() []list.Item { + var items []list.Item + + items = append(items, list.HeaderItem("Agents")) + + // Add all agents (subagents are already filtered out) + for _, agent := range a.allAgents { + items = append(items, agentItem{agent: agent}) + } + + return items +} + +func (a *agentDialog) Render(background string) string { + return a.modal.Render(a.View(), background) +} + +func (s *agentDialog) Close() tea.Cmd { + return nil +} + +func NewAgentDialog(app *app.App) AgentDialog { + dialog := &agentDialog{ + app: app, + } + + dialog.setupAllAgents() + + dialog.modal = modal.New( + modal.WithTitle("Select Agent"), + modal.WithMaxWidth(dialog.dialogWidth+4), + ) + + return dialog +} |
