mirror of
https://github.com/sst/opencode.git
synced 2025-08-24 23:14:10 +00:00

* Implement custom commands * Add User: prefix * Reuse var * Check if the agent is busy and if so report a warning * Update README * fix typo * Implement user and project scoped custom commands * Allow for $ARGUMENTS * UI tweaks * Update internal/tui/components/dialog/arguments.go Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com> * Also search in $HOME/.opencode/commands --------- Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>
166 lines
4.8 KiB
Go
166 lines
4.8 KiB
Go
package dialog
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/opencode-ai/opencode/internal/config"
|
|
"github.com/opencode-ai/opencode/internal/tui/util"
|
|
)
|
|
|
|
// Command prefix constants
|
|
const (
|
|
UserCommandPrefix = "user:"
|
|
ProjectCommandPrefix = "project:"
|
|
)
|
|
|
|
// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
|
|
func LoadCustomCommands() ([]Command, error) {
|
|
cfg := config.Get()
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("config not loaded")
|
|
}
|
|
|
|
var commands []Command
|
|
|
|
// Load user commands from XDG_CONFIG_HOME/opencode/commands
|
|
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
|
if xdgConfigHome == "" {
|
|
// Default to ~/.config if XDG_CONFIG_HOME is not set
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
xdgConfigHome = filepath.Join(home, ".config")
|
|
}
|
|
}
|
|
|
|
if xdgConfigHome != "" {
|
|
userCommandsDir := filepath.Join(xdgConfigHome, "opencode", "commands")
|
|
userCommands, err := loadCommandsFromDir(userCommandsDir, UserCommandPrefix)
|
|
if err != nil {
|
|
// Log error but continue - we'll still try to load other commands
|
|
fmt.Printf("Warning: failed to load user commands from XDG_CONFIG_HOME: %v\n", err)
|
|
} else {
|
|
commands = append(commands, userCommands...)
|
|
}
|
|
}
|
|
|
|
// Load commands from $HOME/.opencode/commands
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
homeCommandsDir := filepath.Join(home, ".opencode", "commands")
|
|
homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
|
|
if err != nil {
|
|
// Log error but continue - we'll still try to load other commands
|
|
fmt.Printf("Warning: failed to load home commands: %v\n", err)
|
|
} else {
|
|
commands = append(commands, homeCommands...)
|
|
}
|
|
}
|
|
|
|
// Load project commands from data directory
|
|
projectCommandsDir := filepath.Join(cfg.Data.Directory, "commands")
|
|
projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
|
|
if err != nil {
|
|
// Log error but return what we have so far
|
|
fmt.Printf("Warning: failed to load project commands: %v\n", err)
|
|
} else {
|
|
commands = append(commands, projectCommands...)
|
|
}
|
|
|
|
return commands, nil
|
|
}
|
|
|
|
// loadCommandsFromDir loads commands from a specific directory with the given prefix
|
|
func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
|
|
// Check if the commands directory exists
|
|
if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
|
|
// Create the commands directory if it doesn't exist
|
|
if err := os.MkdirAll(commandsDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create commands directory %s: %w", commandsDir, err)
|
|
}
|
|
// Return empty list since we just created the directory
|
|
return []Command{}, nil
|
|
}
|
|
|
|
var commands []Command
|
|
|
|
// Walk through the commands directory and load all .md files
|
|
err := filepath.Walk(commandsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip directories
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
// Only process markdown files
|
|
if !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
|
|
return nil
|
|
}
|
|
|
|
// Read the file content
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read command file %s: %w", path, err)
|
|
}
|
|
|
|
// Get the command ID from the file name without the .md extension
|
|
commandID := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
|
|
|
|
// Get relative path from commands directory
|
|
relPath, err := filepath.Rel(commandsDir, path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
|
|
}
|
|
|
|
// Create the command ID from the relative path
|
|
// Replace directory separators with colons
|
|
commandIDPath := strings.ReplaceAll(filepath.Dir(relPath), string(filepath.Separator), ":")
|
|
if commandIDPath != "." {
|
|
commandID = commandIDPath + ":" + commandID
|
|
}
|
|
|
|
// Create a command
|
|
command := Command{
|
|
ID: prefix + commandID,
|
|
Title: prefix + commandID,
|
|
Description: fmt.Sprintf("Custom command from %s", relPath),
|
|
Handler: func(cmd Command) tea.Cmd {
|
|
commandContent := string(content)
|
|
|
|
// Check if the command contains $ARGUMENTS placeholder
|
|
if strings.Contains(commandContent, "$ARGUMENTS") {
|
|
// Show arguments dialog
|
|
return util.CmdHandler(ShowArgumentsDialogMsg{
|
|
CommandID: cmd.ID,
|
|
Content: commandContent,
|
|
})
|
|
}
|
|
|
|
// No arguments needed, run command directly
|
|
return util.CmdHandler(CommandRunCustomMsg{
|
|
Content: commandContent,
|
|
})
|
|
},
|
|
}
|
|
|
|
commands = append(commands, command)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load custom commands from %s: %w", commandsDir, err)
|
|
}
|
|
|
|
return commands, nil
|
|
}
|
|
|
|
// CommandRunCustomMsg is sent when a custom command is executed
|
|
type CommandRunCustomMsg struct {
|
|
Content string
|
|
}
|