mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-05 00:44:54 +01:00
refactor(agent-modal): revamped UI/UX for the agent modal (#1838)
Co-authored-by: Dax Raad <d@ironbay.co> Co-authored-by: Dax <mail@thdxr.com>
This commit is contained in:
committed by
GitHub
parent
d16ae1fc4e
commit
81583cddbd
@@ -71,9 +71,11 @@ type ModelSelectedMsg struct {
|
|||||||
Provider opencode.Provider
|
Provider opencode.Provider
|
||||||
Model opencode.Model
|
Model opencode.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentSelectedMsg struct {
|
type AgentSelectedMsg struct {
|
||||||
Agent opencode.Agent
|
AgentName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionClearedMsg struct{}
|
type SessionClearedMsg struct{}
|
||||||
type CompactSessionMsg struct{}
|
type CompactSessionMsg struct{}
|
||||||
type SendPrompt = Prompt
|
type SendPrompt = Prompt
|
||||||
@@ -272,6 +274,7 @@ func (a *App) cycleMode(forward bool) (*App, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.State.Agent = a.Agent().Name
|
a.State.Agent = a.Agent().Name
|
||||||
|
a.State.UpdateAgentUsage(a.Agent().Name)
|
||||||
return a, a.SaveState()
|
return a, a.SaveState()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +319,45 @@ func (a *App) CycleRecentModel() (*App, tea.Cmd) {
|
|||||||
return a, toast.NewErrorToast("Recent model not found")
|
return a, toast.NewErrorToast("Recent model not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SwitchToAgent(agentName string) (*App, tea.Cmd) {
|
||||||
|
// Find the agent index by name
|
||||||
|
for i, agent := range a.Agents {
|
||||||
|
if agent.Name == agentName {
|
||||||
|
a.AgentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up model for the new agent
|
||||||
|
modelID := a.Agent().Model.ModelID
|
||||||
|
providerID := a.Agent().Model.ProviderID
|
||||||
|
if modelID == "" {
|
||||||
|
if model, ok := a.State.AgentModel[a.Agent().Name]; ok {
|
||||||
|
modelID = model.ModelID
|
||||||
|
providerID = model.ProviderID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if modelID != "" {
|
||||||
|
for _, provider := range a.Providers {
|
||||||
|
if provider.ID == providerID {
|
||||||
|
a.Provider = &provider
|
||||||
|
for _, model := range provider.Models {
|
||||||
|
if model.ID == modelID {
|
||||||
|
a.Model = &model
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.State.Agent = a.Agent().Name
|
||||||
|
a.State.UpdateAgentUsage(agentName)
|
||||||
|
return a, a.SaveState()
|
||||||
|
}
|
||||||
|
|
||||||
// findModelByFullID finds a model by its full ID in the format "provider/model"
|
// findModelByFullID finds a model by its full ID in the format "provider/model"
|
||||||
func findModelByFullID(
|
func findModelByFullID(
|
||||||
providers []opencode.Provider,
|
providers []opencode.Provider,
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ type ModelUsage struct {
|
|||||||
LastUsed time.Time `toml:"last_used"`
|
LastUsed time.Time `toml:"last_used"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentUsage struct {
|
||||||
|
AgentName string `toml:"agent_name"`
|
||||||
|
LastUsed time.Time `toml:"last_used"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentModel struct {
|
type AgentModel struct {
|
||||||
ProviderID string `toml:"provider_id"`
|
ProviderID string `toml:"provider_id"`
|
||||||
ModelID string `toml:"model_id"`
|
ModelID string `toml:"model_id"`
|
||||||
@@ -29,6 +34,7 @@ type State struct {
|
|||||||
Model string `toml:"model"`
|
Model string `toml:"model"`
|
||||||
Agent string `toml:"agent"`
|
Agent string `toml:"agent"`
|
||||||
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
|
||||||
|
RecentlyUsedAgents []AgentUsage `toml:"recently_used_agents"`
|
||||||
MessagesRight bool `toml:"messages_right"`
|
MessagesRight bool `toml:"messages_right"`
|
||||||
SplitDiff bool `toml:"split_diff"`
|
SplitDiff bool `toml:"split_diff"`
|
||||||
MessageHistory []Prompt `toml:"message_history"`
|
MessageHistory []Prompt `toml:"message_history"`
|
||||||
@@ -42,6 +48,7 @@ func NewState() *State {
|
|||||||
Agent: "build",
|
Agent: "build",
|
||||||
AgentModel: make(map[string]AgentModel),
|
AgentModel: make(map[string]AgentModel),
|
||||||
RecentlyUsedModels: make([]ModelUsage, 0),
|
RecentlyUsedModels: make([]ModelUsage, 0),
|
||||||
|
RecentlyUsedAgents: make([]AgentUsage, 0),
|
||||||
MessageHistory: make([]Prompt, 0),
|
MessageHistory: make([]Prompt, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +90,42 @@ func (s *State) RemoveModelFromRecentlyUsed(providerID, modelID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAgentUsage updates the recently used agents list with the specified agent
|
||||||
|
func (s *State) UpdateAgentUsage(agentName string) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Check if this agent is already in the list
|
||||||
|
for i, usage := range s.RecentlyUsedAgents {
|
||||||
|
if usage.AgentName == agentName {
|
||||||
|
s.RecentlyUsedAgents[i].LastUsed = now
|
||||||
|
usage := s.RecentlyUsedAgents[i]
|
||||||
|
copy(s.RecentlyUsedAgents[1:i+1], s.RecentlyUsedAgents[0:i])
|
||||||
|
s.RecentlyUsedAgents[0] = usage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newUsage := AgentUsage{
|
||||||
|
AgentName: agentName,
|
||||||
|
LastUsed: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepend to slice and limit to last 20 entries
|
||||||
|
s.RecentlyUsedAgents = append([]AgentUsage{newUsage}, s.RecentlyUsedAgents...)
|
||||||
|
if len(s.RecentlyUsedAgents) > 20 {
|
||||||
|
s.RecentlyUsedAgents = s.RecentlyUsedAgents[:20]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) RemoveAgentFromRecentlyUsed(agentName string) {
|
||||||
|
for i, usage := range s.RecentlyUsedAgents {
|
||||||
|
if usage.AgentName == agentName {
|
||||||
|
s.RecentlyUsedAgents = append(s.RecentlyUsedAgents[:i], s.RecentlyUsedAgents[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *State) AddPromptToHistory(prompt Prompt) {
|
func (s *State) AddPromptToHistory(prompt Prompt) {
|
||||||
s.MessageHistory = append([]Prompt{prompt}, s.MessageHistory...)
|
s.MessageHistory = append([]Prompt{prompt}, s.MessageHistory...)
|
||||||
if len(s.MessageHistory) > 50 {
|
if len(s.MessageHistory) > 50 {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package dialog
|
package dialog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/v2/key"
|
"github.com/charmbracelet/bubbles/v2/key"
|
||||||
tea "github.com/charmbracelet/bubbletea/v2"
|
tea "github.com/charmbracelet/bubbletea/v2"
|
||||||
@@ -19,9 +19,10 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
numVisibleAgents = 10
|
numVisibleAgents = 10
|
||||||
minAgentDialogWidth = 54
|
minAgentDialogWidth = 40
|
||||||
maxAgentDialogWidth = 108
|
maxAgentDialogWidth = 60
|
||||||
maxDescriptionLength = 80
|
maxDescriptionLength = 60
|
||||||
|
maxRecentAgents = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentDialog interface for the agent selection dialog
|
// AgentDialog interface for the agent selection dialog
|
||||||
@@ -31,7 +32,7 @@ type AgentDialog interface {
|
|||||||
|
|
||||||
type agentDialog struct {
|
type agentDialog struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
allAgents []opencode.Agent
|
allAgents []agentSelectItem
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
modal *modal.Modal
|
modal *modal.Modal
|
||||||
@@ -39,24 +40,31 @@ type agentDialog struct {
|
|||||||
dialogWidth int
|
dialogWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
// agentItem is a custom list item for agent selections
|
// agentSelectItem combines the visual improvements with code patterns
|
||||||
type agentItem struct {
|
type agentSelectItem struct {
|
||||||
agent opencode.Agent
|
name string
|
||||||
|
displayName string
|
||||||
|
description string
|
||||||
|
mode string // "primary", "subagent", "all"
|
||||||
|
isCurrent bool
|
||||||
|
agentIndex int
|
||||||
|
agent opencode.Agent // Keep original agent for compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a agentItem) Render(
|
func (a agentSelectItem) Render(
|
||||||
selected bool,
|
selected bool,
|
||||||
width int,
|
width int,
|
||||||
baseStyle styles.Style,
|
baseStyle styles.Style,
|
||||||
) string {
|
) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
|
||||||
itemStyle := baseStyle.
|
itemStyle := baseStyle.
|
||||||
Background(t.BackgroundPanel()).
|
Background(t.BackgroundPanel()).
|
||||||
Foreground(t.Text())
|
Foreground(t.Text())
|
||||||
|
|
||||||
if selected {
|
if selected {
|
||||||
itemStyle = itemStyle.Foreground(t.Primary())
|
// Use agent color for highlighting when selected (visual improvement)
|
||||||
|
agentColor := util.GetAgentColor(a.agentIndex)
|
||||||
|
itemStyle = itemStyle.Foreground(agentColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
descStyle := baseStyle.
|
descStyle := baseStyle.
|
||||||
@@ -66,25 +74,43 @@ func (a agentItem) Render(
|
|||||||
// Calculate available width (accounting for padding and margins)
|
// Calculate available width (accounting for padding and margins)
|
||||||
availableWidth := width - 2 // Account for left padding
|
availableWidth := width - 2 // Account for left padding
|
||||||
|
|
||||||
agentName := a.agent.Name
|
agentName := a.displayName
|
||||||
description := a.agent.Description
|
|
||||||
if description == "" {
|
// For user agents and subagents, show description; for built-in, show mode
|
||||||
description = fmt.Sprintf("(%s)", a.agent.Mode)
|
var displayText string
|
||||||
|
if a.description != "" && (a.mode == "all" || a.mode == "subagent") {
|
||||||
|
// User agent or subagent with description
|
||||||
|
displayText = a.description
|
||||||
|
} else {
|
||||||
|
// Built-in without description - show mode
|
||||||
|
switch a.mode {
|
||||||
|
case "primary":
|
||||||
|
displayText = "(built-in)"
|
||||||
|
case "all":
|
||||||
|
displayText = "(user)"
|
||||||
|
default:
|
||||||
|
displayText = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
separator := " - "
|
separator := " - "
|
||||||
|
|
||||||
// Calculate how much space we have for the description
|
// Calculate how much space we have for the description (visual improvement)
|
||||||
nameAndSeparatorLength := len(agentName) + len(separator)
|
nameAndSeparatorLength := len(agentName) + len(separator)
|
||||||
descriptionMaxLength := availableWidth - nameAndSeparatorLength
|
descriptionMaxLength := availableWidth - nameAndSeparatorLength
|
||||||
|
|
||||||
// Truncate description if it's too long
|
// Cap description length to the maximum allowed
|
||||||
if len(description) > descriptionMaxLength && descriptionMaxLength > 3 {
|
if descriptionMaxLength > maxDescriptionLength {
|
||||||
description = description[:descriptionMaxLength-3] + "..."
|
descriptionMaxLength = maxDescriptionLength
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate description if it's too long (visual improvement)
|
||||||
|
if len(displayText) > descriptionMaxLength && descriptionMaxLength > 3 {
|
||||||
|
displayText = displayText[:descriptionMaxLength-3] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
namePart := itemStyle.Render(agentName)
|
namePart := itemStyle.Render(agentName)
|
||||||
descPart := descStyle.Render(separator + description)
|
descPart := descStyle.Render(separator + displayText)
|
||||||
combinedText := namePart + descPart
|
combinedText := namePart + descPart
|
||||||
|
|
||||||
return baseStyle.
|
return baseStyle.
|
||||||
@@ -94,8 +120,7 @@ func (a agentItem) Render(
|
|||||||
Render(combinedText)
|
Render(combinedText)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a agentItem) Selectable() bool {
|
func (a agentSelectItem) Selectable() bool {
|
||||||
// All agents in the dialog are selectable (subagents are filtered out)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,32 +147,43 @@ func (a *agentDialog) Init() tea.Cmd {
|
|||||||
|
|
||||||
func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
a.width = msg.Width
|
||||||
|
a.height = msg.Height
|
||||||
|
a.searchDialog.SetWidth(a.dialogWidth)
|
||||||
|
a.searchDialog.SetHeight(msg.Height)
|
||||||
|
|
||||||
case SearchSelectionMsg:
|
case SearchSelectionMsg:
|
||||||
// Handle selection from search dialog
|
// Handle selection from search dialog
|
||||||
if item, ok := msg.Item.(agentItem); ok {
|
if item, ok := msg.Item.(agentSelectItem); ok {
|
||||||
return a, tea.Sequence(
|
if !item.isCurrent {
|
||||||
util.CmdHandler(modal.CloseModalMsg{}),
|
// Switch to selected agent (using their better pattern)
|
||||||
util.CmdHandler(
|
return a, tea.Sequence(
|
||||||
app.AgentSelectedMsg{
|
util.CmdHandler(modal.CloseModalMsg{}),
|
||||||
Agent: item.agent,
|
util.CmdHandler(app.AgentSelectedMsg{AgentName: item.name}),
|
||||||
}),
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
return a, util.CmdHandler(modal.CloseModalMsg{})
|
return a, util.CmdHandler(modal.CloseModalMsg{})
|
||||||
case SearchCancelledMsg:
|
case SearchCancelledMsg:
|
||||||
return a, util.CmdHandler(modal.CloseModalMsg{})
|
return a, util.CmdHandler(modal.CloseModalMsg{})
|
||||||
|
|
||||||
|
case SearchRemoveItemMsg:
|
||||||
|
if item, ok := msg.Item.(agentSelectItem); ok {
|
||||||
|
if a.isAgentInRecentSection(item, msg.Index) {
|
||||||
|
a.app.State.RemoveAgentFromRecentlyUsed(item.name)
|
||||||
|
items := a.buildDisplayList(a.searchDialog.GetQuery())
|
||||||
|
a.searchDialog.SetItems(items)
|
||||||
|
return a, a.app.SaveState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
|
||||||
case SearchQueryChangedMsg:
|
case SearchQueryChangedMsg:
|
||||||
// Update the list based on search query
|
// Update the list based on search query
|
||||||
items := a.buildDisplayList(msg.Query)
|
items := a.buildDisplayList(msg.Query)
|
||||||
a.searchDialog.SetItems(items)
|
a.searchDialog.SetItems(items)
|
||||||
return a, nil
|
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)
|
updatedDialog, cmd := a.searchDialog.Update(msg)
|
||||||
@@ -155,20 +191,38 @@ func (a *agentDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return a, cmd
|
return a, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *agentDialog) SetSize(width, height int) {
|
||||||
|
a.width = width
|
||||||
|
a.height = height
|
||||||
|
}
|
||||||
|
|
||||||
func (a *agentDialog) View() string {
|
func (a *agentDialog) View() string {
|
||||||
return a.searchDialog.View()
|
return a.searchDialog.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agentDialog) calculateOptimalWidth(agents []opencode.Agent) int {
|
func (a *agentDialog) calculateOptimalWidth(agents []agentSelectItem) int {
|
||||||
maxWidth := minAgentDialogWidth
|
maxWidth := minAgentDialogWidth
|
||||||
|
|
||||||
for _, agent := range agents {
|
for _, agent := range agents {
|
||||||
// Calculate the width needed for this item: "AgentName - Description"
|
// Calculate the width needed for this item: "AgentName - Description" (visual improvement)
|
||||||
itemWidth := len(agent.Name)
|
itemWidth := len(agent.displayName)
|
||||||
if agent.Description != "" {
|
if agent.description != "" && (agent.mode == "all" || agent.mode == "subagent") {
|
||||||
itemWidth += len(agent.Description) + 3 // " - "
|
// User agent or subagent - use description (capped to maxDescriptionLength)
|
||||||
|
descLength := len(agent.description)
|
||||||
|
if descLength > maxDescriptionLength {
|
||||||
|
descLength = maxDescriptionLength
|
||||||
|
}
|
||||||
|
itemWidth += descLength + 3 // " - "
|
||||||
} else {
|
} else {
|
||||||
itemWidth += len(string(agent.Mode)) + 3 // " (mode)"
|
// Built-in without description - use mode
|
||||||
|
var modeText string
|
||||||
|
switch agent.mode {
|
||||||
|
case "primary":
|
||||||
|
modeText = "(built-in)"
|
||||||
|
case "all":
|
||||||
|
modeText = "(user)"
|
||||||
|
}
|
||||||
|
itemWidth += len(modeText) + 3 // " - "
|
||||||
}
|
}
|
||||||
|
|
||||||
if itemWidth > maxWidth {
|
if itemWidth > maxWidth {
|
||||||
@@ -177,22 +231,34 @@ func (a *agentDialog) calculateOptimalWidth(agents []opencode.Agent) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = min(maxWidth, maxAgentDialogWidth)
|
maxWidth = min(maxWidth, maxAgentDialogWidth)
|
||||||
|
|
||||||
return maxWidth
|
return maxWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *agentDialog) setupAllAgents() {
|
func (a *agentDialog) setupAllAgents() {
|
||||||
// Get agents from the app, filtering out subagents
|
currentAgentName := a.app.Agent().Name
|
||||||
a.allAgents = []opencode.Agent{}
|
|
||||||
for _, agent := range a.app.Agents {
|
// Build agent items from app.Agents (no API call needed) - their pattern
|
||||||
if agent.Mode != "subagent" {
|
a.allAgents = make([]agentSelectItem, 0, len(a.app.Agents))
|
||||||
a.allAgents = append(a.allAgents, agent)
|
for i, agent := range a.app.Agents {
|
||||||
}
|
isCurrent := agent.Name == currentAgentName
|
||||||
|
|
||||||
|
// Create display name (capitalize first letter)
|
||||||
|
displayName := strings.Title(agent.Name)
|
||||||
|
|
||||||
|
a.allAgents = append(a.allAgents, agentSelectItem{
|
||||||
|
name: agent.Name,
|
||||||
|
displayName: displayName,
|
||||||
|
description: agent.Description, // Keep for search but don't use in display
|
||||||
|
mode: string(agent.Mode),
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
agentIndex: i,
|
||||||
|
agent: agent, // Keep original for compatibility
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
a.sortAgents()
|
a.sortAgents()
|
||||||
|
|
||||||
// Calculate optimal width based on all agents
|
// Calculate optimal width based on all agents (visual improvement)
|
||||||
a.dialogWidth = a.calculateOptimalWidth(a.allAgents)
|
a.dialogWidth = a.calculateOptimalWidth(a.allAgents)
|
||||||
|
|
||||||
// Ensure minimum width to prevent textinput issues
|
// Ensure minimum width to prevent textinput issues
|
||||||
@@ -201,6 +267,7 @@ func (a *agentDialog) setupAllAgents() {
|
|||||||
a.searchDialog = NewSearchDialog("Search agents...", numVisibleAgents)
|
a.searchDialog = NewSearchDialog("Search agents...", numVisibleAgents)
|
||||||
a.searchDialog.SetWidth(a.dialogWidth)
|
a.searchDialog.SetWidth(a.dialogWidth)
|
||||||
|
|
||||||
|
// Build initial display list (empty query shows grouped view)
|
||||||
items := a.buildDisplayList("")
|
items := a.buildDisplayList("")
|
||||||
a.searchDialog.SetItems(items)
|
a.searchDialog.SetItems(items)
|
||||||
}
|
}
|
||||||
@@ -210,42 +277,40 @@ func (a *agentDialog) sortAgents() {
|
|||||||
agentA := a.allAgents[i]
|
agentA := a.allAgents[i]
|
||||||
agentB := a.allAgents[j]
|
agentB := a.allAgents[j]
|
||||||
|
|
||||||
// Current agent goes first
|
// Current agent goes first (your preference)
|
||||||
if agentA.Name == a.app.Agent().Name {
|
if agentA.name == a.app.Agent().Name {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if agentB.Name == a.app.Agent().Name {
|
if agentB.name == a.app.Agent().Name {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alphabetical order for all other agents
|
// Alphabetical order for all other agents
|
||||||
return agentA.Name < agentB.Name
|
return agentA.name < agentB.name
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildDisplayList creates the list items based on search query
|
||||||
func (a *agentDialog) buildDisplayList(query string) []list.Item {
|
func (a *agentDialog) buildDisplayList(query string) []list.Item {
|
||||||
if query != "" {
|
if query != "" {
|
||||||
|
// Search mode: use fuzzy matching
|
||||||
return a.buildSearchResults(query)
|
return a.buildSearchResults(query)
|
||||||
|
} else {
|
||||||
|
// Grouped mode: show Recent agents section and alphabetical list (their pattern)
|
||||||
|
return a.buildGroupedResults()
|
||||||
}
|
}
|
||||||
return a.buildGroupedResults()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildSearchResults creates a flat list of search results using fuzzy matching
|
||||||
func (a *agentDialog) buildSearchResults(query string) []list.Item {
|
func (a *agentDialog) buildSearchResults(query string) []list.Item {
|
||||||
agentNames := []string{}
|
agentNames := []string{}
|
||||||
agentMap := make(map[string]opencode.Agent)
|
agentMap := make(map[string]agentSelectItem)
|
||||||
|
|
||||||
for _, agent := range a.allAgents {
|
for _, agent := range a.allAgents {
|
||||||
// Search by name
|
// Search by name only
|
||||||
searchStr := agent.Name
|
searchStr := agent.name
|
||||||
agentNames = append(agentNames, searchStr)
|
agentNames = append(agentNames, searchStr)
|
||||||
agentMap[searchStr] = agent
|
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)
|
matches := fuzzy.RankFindFold(query, agentNames)
|
||||||
@@ -257,25 +322,74 @@ func (a *agentDialog) buildSearchResults(query string) []list.Item {
|
|||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
agent := agentMap[match.Target]
|
agent := agentMap[match.Target]
|
||||||
// Create a unique key to avoid duplicates
|
// Create a unique key to avoid duplicates
|
||||||
key := agent.Name
|
key := agent.name
|
||||||
if seenAgents[key] {
|
if seenAgents[key] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenAgents[key] = true
|
seenAgents[key] = true
|
||||||
items = append(items, agentItem{agent: agent})
|
items = append(items, agent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildGroupedResults creates a grouped list with Recent agents section and categorized agents
|
||||||
func (a *agentDialog) buildGroupedResults() []list.Item {
|
func (a *agentDialog) buildGroupedResults() []list.Item {
|
||||||
var items []list.Item
|
var items []list.Item
|
||||||
|
|
||||||
items = append(items, list.HeaderItem("Agents"))
|
// Add Recent section (their pattern)
|
||||||
|
recentAgents := a.getRecentAgents(maxRecentAgents)
|
||||||
|
if len(recentAgents) > 0 {
|
||||||
|
items = append(items, list.HeaderItem("Recent"))
|
||||||
|
for _, agent := range recentAgents {
|
||||||
|
items = append(items, agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create map of recent agent names for filtering
|
||||||
|
recentAgentNames := make(map[string]bool)
|
||||||
|
for _, recent := range recentAgents {
|
||||||
|
recentAgentNames[recent.name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separate agents by type (excluding recent ones)
|
||||||
|
primaryAndUserAgents := make([]agentSelectItem, 0)
|
||||||
|
subAgents := make([]agentSelectItem, 0)
|
||||||
|
|
||||||
// Add all agents (subagents are already filtered out)
|
|
||||||
for _, agent := range a.allAgents {
|
for _, agent := range a.allAgents {
|
||||||
items = append(items, agentItem{agent: agent})
|
if !recentAgentNames[agent.name] {
|
||||||
|
switch agent.mode {
|
||||||
|
case "subagent":
|
||||||
|
subAgents = append(subAgents, agent)
|
||||||
|
default:
|
||||||
|
// primary, all, and any other types go in main "Agents" section
|
||||||
|
primaryAndUserAgents = append(primaryAndUserAgents, agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort each category alphabetically
|
||||||
|
sort.Slice(primaryAndUserAgents, func(i, j int) bool {
|
||||||
|
return primaryAndUserAgents[i].name < primaryAndUserAgents[j].name
|
||||||
|
})
|
||||||
|
sort.Slice(subAgents, func(i, j int) bool {
|
||||||
|
return subAgents[i].name < subAgents[j].name
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add main agents section
|
||||||
|
if len(primaryAndUserAgents) > 0 {
|
||||||
|
items = append(items, list.HeaderItem("Agents"))
|
||||||
|
for _, agent := range primaryAndUserAgents {
|
||||||
|
items = append(items, agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subagents section
|
||||||
|
if len(subAgents) > 0 {
|
||||||
|
items = append(items, list.HeaderItem("Subagents"))
|
||||||
|
for _, agent := range subAgents {
|
||||||
|
items = append(items, agent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
@@ -285,10 +399,65 @@ func (a *agentDialog) Render(background string) string {
|
|||||||
return a.modal.Render(a.View(), background)
|
return a.modal.Render(a.View(), background)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *agentDialog) Close() tea.Cmd {
|
func (a *agentDialog) Close() tea.Cmd {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getRecentAgents returns the most recently used agents (their pattern)
|
||||||
|
func (a *agentDialog) getRecentAgents(limit int) []agentSelectItem {
|
||||||
|
var recentAgents []agentSelectItem
|
||||||
|
|
||||||
|
// Get recent agents from app state
|
||||||
|
for _, usage := range a.app.State.RecentlyUsedAgents {
|
||||||
|
if len(recentAgents) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the corresponding agent
|
||||||
|
for _, agent := range a.allAgents {
|
||||||
|
if agent.name == usage.AgentName {
|
||||||
|
recentAgents = append(recentAgents, agent)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no recent agents, use the current agent
|
||||||
|
if len(recentAgents) == 0 {
|
||||||
|
currentAgentName := a.app.Agent().Name
|
||||||
|
for _, agent := range a.allAgents {
|
||||||
|
if agent.name == currentAgentName {
|
||||||
|
recentAgents = append(recentAgents, agent)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return recentAgents
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *agentDialog) isAgentInRecentSection(agent agentSelectItem, index int) bool {
|
||||||
|
// Only check if we're in grouped mode (no search query)
|
||||||
|
if a.searchDialog.GetQuery() != "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
recentAgents := a.getRecentAgents(maxRecentAgents)
|
||||||
|
if len(recentAgents) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index 0 is the "Recent" header, so recent agents are at indices 1 to len(recentAgents)
|
||||||
|
if index >= 1 && index <= len(recentAgents) {
|
||||||
|
if index-1 < len(recentAgents) {
|
||||||
|
recentAgent := recentAgents[index-1]
|
||||||
|
return recentAgent.name == agent.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewAgentDialog(app *app.App) AgentDialog {
|
func NewAgentDialog(app *app.App) AgentDialog {
|
||||||
dialog := &agentDialog{
|
dialog := &agentDialog{
|
||||||
app: app,
|
app: app,
|
||||||
|
|||||||
@@ -599,31 +599,9 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
|
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
|
||||||
cmds = append(cmds, a.app.SaveState())
|
cmds = append(cmds, a.app.SaveState())
|
||||||
case app.AgentSelectedMsg:
|
case app.AgentSelectedMsg:
|
||||||
// Find the agent index
|
updated, cmd := a.app.SwitchToAgent(msg.AgentName)
|
||||||
for i, agent := range a.app.Agents {
|
a.app = updated
|
||||||
if agent.Name == msg.Agent.Name {
|
cmds = append(cmds, cmd)
|
||||||
a.app.AgentIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a.app.State.Agent = msg.Agent.Name
|
|
||||||
|
|
||||||
// Switch to the agent's preferred model if available
|
|
||||||
if model, ok := a.app.State.AgentModel[msg.Agent.Name]; ok {
|
|
||||||
for _, provider := range a.app.Providers {
|
|
||||||
if provider.ID == model.ProviderID {
|
|
||||||
a.app.Provider = &provider
|
|
||||||
for _, m := range provider.Models {
|
|
||||||
if m.ID == model.ModelID {
|
|
||||||
a.app.Model = &m
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmds = append(cmds, a.app.SaveState())
|
|
||||||
case dialog.ThemeSelectedMsg:
|
case dialog.ThemeSelectedMsg:
|
||||||
a.app.State.Theme = msg.ThemeName
|
a.app.State.Theme = msg.ThemeName
|
||||||
cmds = append(cmds, a.app.SaveState())
|
cmds = append(cmds, a.app.SaveState())
|
||||||
@@ -1171,6 +1149,7 @@ func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
|
|||||||
case commands.ModelListCommand:
|
case commands.ModelListCommand:
|
||||||
modelDialog := dialog.NewModelDialog(a.app)
|
modelDialog := dialog.NewModelDialog(a.app)
|
||||||
a.modal = modelDialog
|
a.modal = modelDialog
|
||||||
|
|
||||||
case commands.AgentListCommand:
|
case commands.AgentListCommand:
|
||||||
agentDialog := dialog.NewAgentDialog(a.app)
|
agentDialog := dialog.NewAgentDialog(a.app)
|
||||||
a.modal = agentDialog
|
a.modal = agentDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user