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:
Ed Zynda
2025-05-09 17:33:35 +03:00
committed by adamdottv
parent f92b2b76dc
commit 1f8580553c
5 changed files with 483 additions and 0 deletions

View File

@@ -3,6 +3,8 @@ package tui
import (
"context"
"fmt"
"log/slog"
"strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
@@ -126,6 +128,9 @@ type appModel struct {
showThemeDialog bool
themeDialog dialog.ThemeDialog
showArgumentsDialog bool
argumentsDialog dialog.ArgumentsDialogCmp
}
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)
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...)
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)
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:
// 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 {
case key.Matches(msg, keys.Quit):
a.showQuit = !a.showQuit
@@ -327,6 +368,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.showModelDialog {
a.showModelDialog = false
}
if a.showArgumentsDialog {
a.showArgumentsDialog = false
}
return a, nil
case key.Matches(msg, keys.SwitchSession):
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
}
@@ -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
}