mirror of
https://github.com/sst/opencode.git
synced 2025-08-25 07:24:05 +00:00
feat: custom commands (#133)
* 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>
This commit is contained in:
parent
f92b2b76dc
commit
1f8580553c
5 changed files with 483 additions and 0 deletions
64
README.md
64
README.md
|
@ -387,6 +387,70 @@ OpenCode is built with a modular architecture:
|
||||||
- **internal/session**: Session management
|
- **internal/session**: Session management
|
||||||
- **internal/lsp**: Language Server Protocol integration
|
- **internal/lsp**: Language Server Protocol integration
|
||||||
|
|
||||||
|
## Custom Commands
|
||||||
|
|
||||||
|
OpenCode supports custom commands that can be created by users to quickly send predefined prompts to the AI assistant.
|
||||||
|
|
||||||
|
### Creating Custom Commands
|
||||||
|
|
||||||
|
Custom commands are predefined prompts stored as Markdown files in one of three locations:
|
||||||
|
|
||||||
|
1. **User Commands** (prefixed with `user:`):
|
||||||
|
```
|
||||||
|
$XDG_CONFIG_HOME/opencode/commands/
|
||||||
|
```
|
||||||
|
(typically `~/.config/opencode/commands/` on Linux/macOS)
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
$HOME/.opencode/commands/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Project Commands** (prefixed with `project:`):
|
||||||
|
```
|
||||||
|
<PROJECT DIR>/.opencode/commands/
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `.md` file in these directories becomes a custom command. The file name (without extension) becomes the command ID.
|
||||||
|
|
||||||
|
For example, creating a file at `~/.config/opencode/commands/prime-context.md` with content:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
RUN git ls-files
|
||||||
|
READ README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a command called `user:prime-context`.
|
||||||
|
|
||||||
|
### Command Arguments
|
||||||
|
|
||||||
|
You can create commands that accept arguments by including the `$ARGUMENTS` placeholder in your command file:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
RUN git show $ARGUMENTS
|
||||||
|
```
|
||||||
|
|
||||||
|
When you run this command, OpenCode will prompt you to enter the text that should replace `$ARGUMENTS`.
|
||||||
|
|
||||||
|
### Organizing Commands
|
||||||
|
|
||||||
|
You can organize commands in subdirectories:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.config/opencode/commands/git/commit.md
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a command with ID `user:git:commit`.
|
||||||
|
|
||||||
|
### Using Custom Commands
|
||||||
|
|
||||||
|
1. Press `Ctrl+K` to open the command dialog
|
||||||
|
2. Select your custom command (prefixed with either `user:` or `project:`)
|
||||||
|
3. Press Enter to execute the command
|
||||||
|
|
||||||
|
The content of the command file will be sent as a message to the AI assistant.
|
||||||
|
|
||||||
## MCP (Model Context Protocol)
|
## MCP (Model Context Protocol)
|
||||||
|
|
||||||
OpenCode implements the Model Context Protocol (MCP) to extend its capabilities through external tools. MCP provides a standardized way for the AI assistant to interact with external services and tools.
|
OpenCode implements the Model Context Protocol (MCP) to extend its capabilities through external tools. MCP provides a standardized way for the AI assistant to interact with external services and tools.
|
||||||
|
|
173
internal/tui/components/dialog/arguments.go
Normal file
173
internal/tui/components/dialog/arguments.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
package dialog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||||
|
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||||
|
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ArgumentsDialogCmp is a component that asks the user for command arguments.
|
||||||
|
type ArgumentsDialogCmp struct {
|
||||||
|
width, height int
|
||||||
|
textInput textinput.Model
|
||||||
|
keys argumentsDialogKeyMap
|
||||||
|
commandID string
|
||||||
|
content string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArgumentsDialogCmp creates a new ArgumentsDialogCmp.
|
||||||
|
func NewArgumentsDialogCmp(commandID, content string) ArgumentsDialogCmp {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = "Enter arguments..."
|
||||||
|
ti.Focus()
|
||||||
|
ti.Width = 40
|
||||||
|
ti.Prompt = ""
|
||||||
|
ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
|
||||||
|
ti.PromptStyle = ti.PromptStyle.Background(t.Background())
|
||||||
|
ti.TextStyle = ti.TextStyle.Background(t.Background())
|
||||||
|
|
||||||
|
return ArgumentsDialogCmp{
|
||||||
|
textInput: ti,
|
||||||
|
keys: argumentsDialogKeyMap{},
|
||||||
|
commandID: commandID,
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type argumentsDialogKeyMap struct {
|
||||||
|
Enter key.Binding
|
||||||
|
Escape key.Binding
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShortHelp implements key.Map.
|
||||||
|
func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
|
||||||
|
return []key.Binding{
|
||||||
|
key.NewBinding(
|
||||||
|
key.WithKeys("enter"),
|
||||||
|
key.WithHelp("enter", "confirm"),
|
||||||
|
),
|
||||||
|
key.NewBinding(
|
||||||
|
key.WithKeys("esc"),
|
||||||
|
key.WithHelp("esc", "cancel"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullHelp implements key.Map.
|
||||||
|
func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
|
||||||
|
return [][]key.Binding{k.ShortHelp()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init implements tea.Model.
|
||||||
|
func (m ArgumentsDialogCmp) Init() tea.Cmd {
|
||||||
|
return tea.Batch(
|
||||||
|
textinput.Blink,
|
||||||
|
m.textInput.Focus(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements tea.Model.
|
||||||
|
func (m ArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
|
||||||
|
return m, util.CmdHandler(CloseArgumentsDialogMsg{})
|
||||||
|
case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
|
||||||
|
return m, util.CmdHandler(CloseArgumentsDialogMsg{
|
||||||
|
Submit: true,
|
||||||
|
CommandID: m.commandID,
|
||||||
|
Content: m.content,
|
||||||
|
Arguments: m.textInput.Value(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
m.textInput, cmd = m.textInput.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// View implements tea.Model.
|
||||||
|
func (m ArgumentsDialogCmp) View() string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
baseStyle := styles.BaseStyle()
|
||||||
|
|
||||||
|
// Calculate width needed for content
|
||||||
|
maxWidth := 60 // Width for explanation text
|
||||||
|
|
||||||
|
title := baseStyle.
|
||||||
|
Foreground(t.Primary()).
|
||||||
|
Bold(true).
|
||||||
|
Width(maxWidth).
|
||||||
|
Padding(0, 1).
|
||||||
|
Render("Command Arguments")
|
||||||
|
|
||||||
|
explanation := baseStyle.
|
||||||
|
Foreground(t.Text()).
|
||||||
|
Width(maxWidth).
|
||||||
|
Padding(0, 1).
|
||||||
|
Render("This command requires arguments. Please enter the text to replace $ARGUMENTS with:")
|
||||||
|
|
||||||
|
inputField := baseStyle.
|
||||||
|
Foreground(t.Text()).
|
||||||
|
Width(maxWidth).
|
||||||
|
Padding(1, 1).
|
||||||
|
Render(m.textInput.View())
|
||||||
|
|
||||||
|
maxWidth = min(maxWidth, m.width-10)
|
||||||
|
|
||||||
|
content := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
title,
|
||||||
|
explanation,
|
||||||
|
inputField,
|
||||||
|
)
|
||||||
|
|
||||||
|
return baseStyle.Padding(1, 2).
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderBackground(t.Background()).
|
||||||
|
BorderForeground(t.TextMuted()).
|
||||||
|
Background(t.Background()).
|
||||||
|
Width(lipgloss.Width(content) + 4).
|
||||||
|
Render(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSize sets the size of the component.
|
||||||
|
func (m *ArgumentsDialogCmp) SetSize(width, height int) {
|
||||||
|
m.width = width
|
||||||
|
m.height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bindings implements layout.Bindings.
|
||||||
|
func (m ArgumentsDialogCmp) Bindings() []key.Binding {
|
||||||
|
return m.keys.ShortHelp()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
|
||||||
|
type CloseArgumentsDialogMsg struct {
|
||||||
|
Submit bool
|
||||||
|
CommandID string
|
||||||
|
Content string
|
||||||
|
Arguments string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
|
||||||
|
type ShowArgumentsDialogMsg struct {
|
||||||
|
CommandID string
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
166
internal/tui/components/dialog/custom_commands.go
Normal file
166
internal/tui/components/dialog/custom_commands.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/opencode-ai/opencode/internal/session"
|
"github.com/opencode-ai/opencode/internal/session"
|
||||||
"github.com/opencode-ai/opencode/internal/status"
|
"github.com/opencode-ai/opencode/internal/status"
|
||||||
"github.com/opencode-ai/opencode/internal/tui/components/chat"
|
"github.com/opencode-ai/opencode/internal/tui/components/chat"
|
||||||
|
"github.com/opencode-ai/opencode/internal/tui/components/dialog"
|
||||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||||
)
|
)
|
||||||
|
@ -64,6 +65,16 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
return p, cmd
|
return p, cmd
|
||||||
}
|
}
|
||||||
|
case dialog.CommandRunCustomMsg:
|
||||||
|
// Check if the agent is busy before executing custom commands
|
||||||
|
if p.app.CoderAgent.IsBusy() {
|
||||||
|
return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
|
||||||
|
}
|
||||||
|
// Handle custom command execution
|
||||||
|
cmd := p.sendMessage(msg.Content)
|
||||||
|
if cmd != nil {
|
||||||
|
return p, cmd
|
||||||
|
}
|
||||||
case chat.SessionSelectedMsg:
|
case chat.SessionSelectedMsg:
|
||||||
if p.session.ID == "" {
|
if p.session.ID == "" {
|
||||||
cmd := p.setSidebar()
|
cmd := p.setSidebar()
|
||||||
|
|
|
@ -3,6 +3,8 @@ package tui
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
@ -126,6 +128,9 @@ type appModel struct {
|
||||||
|
|
||||||
showThemeDialog bool
|
showThemeDialog bool
|
||||||
themeDialog dialog.ThemeDialog
|
themeDialog dialog.ThemeDialog
|
||||||
|
|
||||||
|
showArgumentsDialog bool
|
||||||
|
argumentsDialog dialog.ArgumentsDialogCmp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a appModel) Init() tea.Cmd {
|
func (a appModel) Init() tea.Cmd {
|
||||||
|
@ -199,6 +204,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
a.initDialog.SetSize(msg.Width, msg.Height)
|
a.initDialog.SetSize(msg.Width, msg.Height)
|
||||||
|
|
||||||
|
if a.showArgumentsDialog {
|
||||||
|
a.argumentsDialog.SetSize(msg.Width, msg.Height)
|
||||||
|
args, argsCmd := a.argumentsDialog.Update(msg)
|
||||||
|
a.argumentsDialog = args.(dialog.ArgumentsDialogCmp)
|
||||||
|
cmds = append(cmds, argsCmd, a.argumentsDialog.Init())
|
||||||
|
}
|
||||||
|
|
||||||
return a, tea.Batch(cmds...)
|
return a, tea.Batch(cmds...)
|
||||||
|
|
||||||
case pubsub.Event[logging.Log]:
|
case pubsub.Event[logging.Log]:
|
||||||
|
@ -307,7 +319,36 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
status.Info("Command selected: " + msg.Command.Title)
|
status.Info("Command selected: " + msg.Command.Title)
|
||||||
return a, nil
|
return a, nil
|
||||||
|
|
||||||
|
case dialog.ShowArgumentsDialogMsg:
|
||||||
|
// Show arguments dialog
|
||||||
|
a.argumentsDialog = dialog.NewArgumentsDialogCmp(msg.CommandID, msg.Content)
|
||||||
|
a.showArgumentsDialog = true
|
||||||
|
return a, a.argumentsDialog.Init()
|
||||||
|
|
||||||
|
case dialog.CloseArgumentsDialogMsg:
|
||||||
|
// Close arguments dialog
|
||||||
|
a.showArgumentsDialog = false
|
||||||
|
|
||||||
|
// If submitted, replace $ARGUMENTS and run the command
|
||||||
|
if msg.Submit {
|
||||||
|
// Replace $ARGUMENTS with the provided arguments
|
||||||
|
content := strings.ReplaceAll(msg.Content, "$ARGUMENTS", msg.Arguments)
|
||||||
|
|
||||||
|
// Execute the command with arguments
|
||||||
|
return a, util.CmdHandler(dialog.CommandRunCustomMsg{
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
// If arguments dialog is open, let it handle the key press first
|
||||||
|
if a.showArgumentsDialog {
|
||||||
|
args, cmd := a.argumentsDialog.Update(msg)
|
||||||
|
a.argumentsDialog = args.(dialog.ArgumentsDialogCmp)
|
||||||
|
return a, cmd
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case key.Matches(msg, keys.Quit):
|
case key.Matches(msg, keys.Quit):
|
||||||
a.showQuit = !a.showQuit
|
a.showQuit = !a.showQuit
|
||||||
|
@ -327,6 +368,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if a.showModelDialog {
|
if a.showModelDialog {
|
||||||
a.showModelDialog = false
|
a.showModelDialog = false
|
||||||
}
|
}
|
||||||
|
if a.showArgumentsDialog {
|
||||||
|
a.showArgumentsDialog = false
|
||||||
|
}
|
||||||
return a, nil
|
return a, nil
|
||||||
case key.Matches(msg, keys.SwitchSession):
|
case key.Matches(msg, keys.SwitchSession):
|
||||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
|
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
|
||||||
|
@ -718,6 +762,21 @@ func (a appModel) View() string {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if a.showArgumentsDialog {
|
||||||
|
overlay := a.argumentsDialog.View()
|
||||||
|
row := lipgloss.Height(appView) / 2
|
||||||
|
row -= lipgloss.Height(overlay) / 2
|
||||||
|
col := lipgloss.Width(appView) / 2
|
||||||
|
col -= lipgloss.Width(overlay) / 2
|
||||||
|
appView = layout.PlaceOverlay(
|
||||||
|
col,
|
||||||
|
row,
|
||||||
|
overlay,
|
||||||
|
appView,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return appView
|
return appView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -781,5 +840,15 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Load custom commands
|
||||||
|
customCommands, err := dialog.LoadCustomCommands()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to load custom commands", "error", err)
|
||||||
|
} else {
|
||||||
|
for _, cmd := range customCommands {
|
||||||
|
model.RegisterCommand(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue