Merge agent and mode into one (#1689)

The concept of mode has been deprecated, there is now only the agent field in the config.

An agent can be cycled through as your primary agent with <tab> or you can spawn a subagent by @ mentioning it. if you include a description of when to use it, the primary agent will try to automatically use it

Full docs here: https://opencode.ai/docs/agents/
This commit is contained in:
Dax
2025-08-07 16:32:12 -04:00
committed by GitHub
parent 12f1ad521f
commit c34aec060f
42 changed files with 1755 additions and 930 deletions

View File

@@ -288,6 +288,31 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
return m, nil
case "agents":
atIndex := m.textarea.LastRuneIndex('@')
if atIndex == -1 {
// Should not happen, but as a fallback, just insert.
m.textarea.InsertString(msg.Item.Value + " ")
return m, nil
}
cursorCol := m.textarea.CursorColumn()
m.textarea.ReplaceRange(atIndex, cursorCol, "")
name := msg.Item.Value
attachment := &attachment.Attachment{
ID: uuid.NewString(),
Type: "agent",
Display: "@" + name,
Source: &attachment.AgentSource{
Name: name,
},
}
m.textarea.InsertAttachment(attachment)
m.textarea.InsertString(" ")
return m, nil
default:
slog.Debug("Unknown provider", "provider", msg.Item.ProviderID)
return m, nil

View File

@@ -209,6 +209,7 @@ func renderText(
width int,
extra string,
fileParts []opencode.FilePart,
agentParts []opencode.AgentPart,
toolCalls ...opencode.ToolPart,
) string {
t := theme.CurrentTheme()
@@ -229,9 +230,47 @@ func renderText(
// Apply highlighting to filenames and base style to rest of text BEFORE wrapping
textLen := int64(len(text))
// Collect all parts to highlight (both file and agent parts)
type highlightPart struct {
start int64
end int64
color compat.AdaptiveColor
}
var highlights []highlightPart
// Add file parts with secondary color
for _, filePart := range fileParts {
highlight := base.Foreground(t.Secondary())
start, end := filePart.Source.Text.Start, filePart.Source.Text.End
highlights = append(highlights, highlightPart{
start: filePart.Source.Text.Start,
end: filePart.Source.Text.End,
color: t.Secondary(),
})
}
// Add agent parts with secondary color (same as file parts)
for _, agentPart := range agentParts {
highlights = append(highlights, highlightPart{
start: agentPart.Source.Start,
end: agentPart.Source.End,
color: t.Secondary(),
})
}
// Sort highlights by start position
slices.SortFunc(highlights, func(a, b highlightPart) int {
if a.start < b.start {
return -1
}
if a.start > b.start {
return 1
}
return 0
})
for _, part := range highlights {
highlight := base.Foreground(part.color)
start, end := part.start, part.end
if end > textLen {
end = textLen

View File

@@ -300,12 +300,17 @@ func (m *messagesComponent) renderView() tea.Cmd {
}
remainingParts := message.Parts[partIndex+1:]
fileParts := make([]opencode.FilePart, 0)
agentParts := make([]opencode.AgentPart, 0)
for _, part := range remainingParts {
switch part := part.(type) {
case opencode.FilePart:
if part.Source.Text.Start >= 0 && part.Source.Text.End >= part.Source.Text.Start {
fileParts = append(fileParts, part)
}
case opencode.AgentPart:
if part.Source.Start >= 0 && part.Source.End >= part.Source.Start {
agentParts = append(agentParts, part)
}
}
}
flexItems := []layout.FlexItem{}
@@ -355,6 +360,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
width,
files,
fileParts,
agentParts,
)
content = lipgloss.PlaceHorizontal(
m.width,
@@ -433,6 +439,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
width,
"",
[]opencode.FilePart{},
[]opencode.AgentPart{},
toolCallParts...,
)
content = lipgloss.PlaceHorizontal(
@@ -453,6 +460,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
width,
"",
[]opencode.FilePart{},
[]opencode.AgentPart{},
toolCallParts...,
)
content = lipgloss.PlaceHorizontal(

View File

@@ -66,11 +66,16 @@ func (c *completionDialogComponent) Init() tea.Cmd {
func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
return func() tea.Msg {
allItems := make([]completions.CompletionSuggestion, 0)
// Collect results from all providers and preserve provider order
type providerItems struct {
idx int
items []completions.CompletionSuggestion
}
itemsByProvider := make([]providerItems, 0, len(c.providers))
providersWithResults := 0
// Collect results from all providers
for _, provider := range c.providers {
for idx, provider := range c.providers {
items, err := provider.GetChildEntries(query)
if err != nil {
slog.Error(
@@ -84,33 +89,46 @@ func (c *completionDialogComponent) getAllCompletions(query string) tea.Cmd {
}
if len(items) > 0 {
providersWithResults++
allItems = append(allItems, items...)
itemsByProvider = append(itemsByProvider, providerItems{idx: idx, items: items})
}
}
// If there's a query, use fuzzy ranking to sort results
if query != "" && providersWithResults > 1 {
// If there's a query, fuzzy-rank within each provider, then concatenate by provider order
if query != "" && providersWithResults > 0 {
t := theme.CurrentTheme()
baseStyle := styles.NewStyle().Background(t.BackgroundElement())
// Create a slice of display values for fuzzy matching
displayValues := make([]string, len(allItems))
for i, item := range allItems {
displayValues[i] = item.Display(baseStyle)
// Ensure stable provider order just in case
sort.SliceStable(itemsByProvider, func(i, j int) bool { return itemsByProvider[i].idx < itemsByProvider[j].idx })
final := make([]completions.CompletionSuggestion, 0)
for _, entry := range itemsByProvider {
// Build display values for fuzzy matching within this provider
displayValues := make([]string, len(entry.items))
for i, item := range entry.items {
displayValues[i] = item.Display(baseStyle)
}
matches := fuzzy.RankFindFold(query, displayValues)
sort.Sort(matches)
// Reorder items for this provider based on fuzzy ranking
ranked := make([]completions.CompletionSuggestion, 0, len(matches))
for _, m := range matches {
ranked = append(ranked, entry.items[m.OriginalIndex])
}
final = append(final, ranked...)
}
matches := fuzzy.RankFindFold(query, displayValues)
sort.Sort(matches)
// Reorder items based on fuzzy ranking
rankedItems := make([]completions.CompletionSuggestion, 0, len(matches))
for _, match := range matches {
rankedItems = append(rankedItems, allItems[match.OriginalIndex])
}
return rankedItems
return final
}
return allItems
// No query or no results: just concatenate in provider order
all := make([]completions.CompletionSuggestion, 0)
for _, entry := range itemsByProvider {
all = append(all, entry.items...)
}
return all
}
}
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

View File

@@ -121,7 +121,7 @@ func (m *statusComponent) View() string {
var modeBackground compat.AdaptiveColor
var modeForeground compat.AdaptiveColor
switch m.app.ModeIndex {
switch m.app.AgentIndex {
case 0:
modeBackground = t.BackgroundElement()
modeForeground = t.TextMuted()
@@ -148,31 +148,31 @@ func (m *statusComponent) View() string {
modeForeground = t.BackgroundPanel()
}
command := m.app.Commands[commands.SwitchModeCommand]
command := m.app.Commands[commands.SwitchAgentCommand]
kb := command.Keybindings[0]
key := kb.Key
if kb.RequiresLeader {
key = m.app.Config.Keybinds.Leader + " " + kb.Key
}
modeStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
modeNameStyle := modeStyle.Bold(true).Render
modeDescStyle := modeStyle.Render
mode := modeNameStyle(strings.ToUpper(m.app.Mode.Name)) + modeDescStyle(" MODE")
mode = modeStyle.
agentStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
agentNameStyle := agentStyle.Bold(true).Render
agentDescStyle := agentStyle.Render
agent := agentNameStyle(strings.ToUpper(m.app.Agent().Name)) + agentDescStyle(" AGENT")
agent = agentStyle.
Padding(0, 1).
BorderLeft(true).
BorderStyle(lipgloss.ThickBorder()).
BorderForeground(modeBackground).
BorderBackground(t.BackgroundPanel()).
Render(mode)
Render(agent)
faintStyle := styles.NewStyle().
Faint(true).
Background(t.BackgroundPanel()).
Foreground(t.TextMuted())
mode = faintStyle.Render(key+" ") + mode
modeWidth := lipgloss.Width(mode)
agent = faintStyle.Render(key+" ") + agent
modeWidth := lipgloss.Width(agent)
availableWidth := m.width - logoWidth - modeWidth
branchSuffix := ""
@@ -206,7 +206,7 @@ func (m *statusComponent) View() string {
View: logo + cwd,
},
layout.FlexItem{
View: mode,
View: agent,
},
)