on main help sign in
feat: Support named arguments in custom commands (#158)
36faca4Ed Zynda
README.md+13−3
@@ -19,6 +19,7 @@ OpenCode is a Go-based CLI application that brings AI assistance to your termina
19- **LSP Integration**: Language Server Protocol support for code intelligence
19- **LSP Integration**: Language Server Protocol support for code intelligence
20- **File Change Tracking**: Track and visualize file changes during sessions
20- **File Change Tracking**: Track and visualize file changes during sessions
21- **External Editor Support**: Open your preferred editor for composing messages
21- **External Editor Support**: Open your preferred editor for composing messages
22+- **Named Arguments for Custom Commands**: Create powerful custom commands with multiple named placeholders
22
23
23## Installation
24## Installation
24
25
@@ -375,13 +376,22 @@ This creates a command called `user:prime-context`.
375
376
376### Command Arguments
377### Command Arguments
377
378
378−You can create commands that accept arguments by including the `$ARGUMENTS` placeholder in your command file:
379+OpenCode supports named arguments in custom commands using placeholders in the format `$NAME` (where NAME consists of uppercase letters, numbers, and underscores, and must start with a letter).
380+
381+For example:
379
382
380```markdown
383```markdown
381−RUN git show $ARGUMENTS
384+# Fetch Context for Issue $ISSUE_NUMBER
385+
386+RUN gh issue view $ISSUE_NUMBER --json title,body,comments
387+RUN git grep --author="$AUTHOR_NAME" -n .
388+RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
382```
389```
383
390
384−When you run this command, OpenCode will prompt you to enter the text that should replace `$ARGUMENTS`.
391+When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
392+- Clear identification of what each argument represents
393+- Ability to use the same argument multiple times
394+- Better organization for commands with multiple inputs
385
395
386### Organizing Commands
396### Organizing Commands
387
397
internal/tui/components/dialog/arguments.go+156−72
@@ -1,6 +1,7 @@
1package dialog
1package dialog
2
2
3import (
3import (
4+ "fmt"
4 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/key"
5 "github.com/charmbracelet/bubbles/textinput"
6 "github.com/charmbracelet/bubbles/textinput"
6 tea "github.com/charmbracelet/bubbletea"
7 tea "github.com/charmbracelet/bubbletea"
@@ -11,35 +12,6 @@ import (
11 "github.com/opencode-ai/opencode/internal/tui/util"
12 "github.com/opencode-ai/opencode/internal/tui/util"
12)
13)
13
14
14−// ArgumentsDialogCmp is a component that asks the user for command arguments.
15−type ArgumentsDialogCmp struct {
16− width, height int
17− textInput textinput.Model
18− keys argumentsDialogKeyMap
19− commandID string
20− content string
21−}
22−
23−// NewArgumentsDialogCmp creates a new ArgumentsDialogCmp.
24−func NewArgumentsDialogCmp(commandID, content string) ArgumentsDialogCmp {
25− t := theme.CurrentTheme()
26− ti := textinput.New()
27− ti.Placeholder = "Enter arguments..."
28− ti.Focus()
29− ti.Width = 40
30− ti.Prompt = ""
31− ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
32− ti.PromptStyle = ti.PromptStyle.Background(t.Background())
33− ti.TextStyle = ti.TextStyle.Background(t.Background())
34−
35− return ArgumentsDialogCmp{
36− textInput: ti,
37− keys: argumentsDialogKeyMap{},
38− commandID: commandID,
39− content: content,
40− }
41−}
42−
43type argumentsDialogKeyMap struct {
15type argumentsDialogKeyMap struct {
44 Enter key.Binding
16 Enter key.Binding
45 Escape key.Binding
17 Escape key.Binding
@@ -64,77 +36,204 @@ func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
64 return [][]key.Binding{k.ShortHelp()}
36 return [][]key.Binding{k.ShortHelp()}
65}
37}
66
38
39+// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
40+type ShowMultiArgumentsDialogMsg struct {
41+ CommandID string
42+ Content string
43+ ArgNames []string
44+}
45+
46+// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
47+type CloseMultiArgumentsDialogMsg struct {
48+ Submit bool
49+ CommandID string
50+ Content string
51+ Args map[string]string
52+}
53+
54+// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
55+type MultiArgumentsDialogCmp struct {
56+ width, height int
57+ inputs []textinput.Model
58+ focusIndex int
59+ keys argumentsDialogKeyMap
60+ commandID string
61+ content string
62+ argNames []string
63+}
64+
65+// NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
66+func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
67+ t := theme.CurrentTheme()
68+ inputs := make([]textinput.Model, len(argNames))
69+
70+ for i, name := range argNames {
71+ ti := textinput.New()
72+ ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
73+ ti.Width = 40
74+ ti.Prompt = ""
75+ ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
76+ ti.PromptStyle = ti.PromptStyle.Background(t.Background())
77+ ti.TextStyle = ti.TextStyle.Background(t.Background())
78+
79+ // Only focus the first input initially
80+ if i == 0 {
81+ ti.Focus()
82+ ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
83+ ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
84+ } else {
85+ ti.Blur()
86+ }
87+
88+ inputs[i] = ti
89+ }
90+
91+ return MultiArgumentsDialogCmp{
92+ inputs: inputs,
93+ keys: argumentsDialogKeyMap{},
94+ commandID: commandID,
95+ content: content,
96+ argNames: argNames,
97+ focusIndex: 0,
98+ }
99+}
100+
67// Init implements tea.Model.
101// Init implements tea.Model.
68−func (m ArgumentsDialogCmp) Init() tea.Cmd {
102+func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
69− return tea.Batch(
103+ // Make sure only the first input is focused
104+ for i := range m.inputs {
105+ if i == 0 {
106+ m.inputs[i].Focus()
107+ } else {
108+ m.inputs[i].Blur()
109+ }
110+ }
111+
70− textinput.Blink,
112+ return textinput.Blink
71− m.textInput.Focus(),
72− )
73}
113}
74
114
75// Update implements tea.Model.
115// Update implements tea.Model.
76−func (m ArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116+func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
77− var cmd tea.Cmd
78 var cmds []tea.Cmd
117 var cmds []tea.Cmd
118+ t := theme.CurrentTheme()
79
119
80 switch msg := msg.(type) {
120 switch msg := msg.(type) {
81 case tea.KeyMsg:
121 case tea.KeyMsg:
82 switch {
122 switch {
83 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
123 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
84− return m, util.CmdHandler(CloseArgumentsDialogMsg{})
124+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
85− case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
86− return m, util.CmdHandler(CloseArgumentsDialogMsg{
87− Submit: true,
125+ Submit: false,
88 CommandID: m.commandID,
126 CommandID: m.commandID,
89 Content: m.content,
127 Content: m.content,
90− Arguments: m.textInput.Value(),
128+ Args: nil,
91 })
129 })
130+ case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
131+ // If we're on the last input, submit the form
132+ if m.focusIndex == len(m.inputs)-1 {
133+ args := make(map[string]string)
134+ for i, name := range m.argNames {
135+ args[name] = m.inputs[i].Value()
136+ }
137+ return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
138+ Submit: true,
139+ CommandID: m.commandID,
140+ Content: m.content,
141+ Args: args,
142+ })
143+ }
144+ // Otherwise, move to the next input
145+ m.inputs[m.focusIndex].Blur()
146+ m.focusIndex++
147+ m.inputs[m.focusIndex].Focus()
148+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
149+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
150+ case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
151+ // Move to the next input
152+ m.inputs[m.focusIndex].Blur()
153+ m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
154+ m.inputs[m.focusIndex].Focus()
155+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
156+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
157+ case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
158+ // Move to the previous input
159+ m.inputs[m.focusIndex].Blur()
160+ m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
161+ m.inputs[m.focusIndex].Focus()
162+ m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
163+ m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
92 }
164 }
93 case tea.WindowSizeMsg:
165 case tea.WindowSizeMsg:
94 m.width = msg.Width
166 m.width = msg.Width
95 m.height = msg.Height
167 m.height = msg.Height
96 }
168 }
97
169
98− m.textInput, cmd = m.textInput.Update(msg)
170+ // Update the focused input
171+ var cmd tea.Cmd
172+ m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
99 cmds = append(cmds, cmd)
173 cmds = append(cmds, cmd)
100
174
101 return m, tea.Batch(cmds...)
175 return m, tea.Batch(cmds...)
102}
176}
103
177
104// View implements tea.Model.
178// View implements tea.Model.
105−func (m ArgumentsDialogCmp) View() string {
179+func (m MultiArgumentsDialogCmp) View() string {
106 t := theme.CurrentTheme()
180 t := theme.CurrentTheme()
107 baseStyle := styles.BaseStyle()
181 baseStyle := styles.BaseStyle()
108
182
109 // Calculate width needed for content
183 // Calculate width needed for content
110 maxWidth := 60 // Width for explanation text
184 maxWidth := 60 // Width for explanation text
111
185
112− title := baseStyle.
186+ title := lipgloss.NewStyle().
113 Foreground(t.Primary()).
187 Foreground(t.Primary()).
114 Bold(true).
188 Bold(true).
115 Width(maxWidth).
189 Width(maxWidth).
116 Padding(0, 1).
190 Padding(0, 1).
191+ Background(t.Background()).
117 Render("Command Arguments")
192 Render("Command Arguments")
118
193
119− explanation := baseStyle.
194+ explanation := lipgloss.NewStyle().
120 Foreground(t.Text()).
195 Foreground(t.Text()).
121 Width(maxWidth).
196 Width(maxWidth).
122 Padding(0, 1).
197 Padding(0, 1).
198+ Background(t.Background()).
123− Render("This command requires arguments. Please enter the text to replace $ARGUMENTS with:")
199+ Render("This command requires multiple arguments. Please enter values for each:")
124
200
125− inputField := baseStyle.
201+ // Create input fields for each argument
202+ inputFields := make([]string, len(m.inputs))
203+ for i, input := range m.inputs {
204+ // Highlight the label of the focused input
205+ labelStyle := lipgloss.NewStyle().
206+ Width(maxWidth).
207+ Padding(1, 1, 0, 1).
208+ Background(t.Background())
209+
210+ if i == m.focusIndex {
211+ labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
212+ } else {
213+ labelStyle = labelStyle.Foreground(t.TextMuted())
214+ }
215+
216+ label := labelStyle.Render(m.argNames[i] + ":")
217+
218+ field := lipgloss.NewStyle().
126− Foreground(t.Text()).
219+ Foreground(t.Text()).
127− Width(maxWidth).
220+ Width(maxWidth).
128− Padding(1, 1).
221+ Padding(0, 1).
129− Render(m.textInput.View())
222+ Background(t.Background()).
223+ Render(input.View())
224+
225+ inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
226+ }
130
227
131 maxWidth = min(maxWidth, m.width-10)
228 maxWidth = min(maxWidth, m.width-10)
132
229
230+ // Join all elements vertically
231+ elements := []string{title, explanation}
232+ elements = append(elements, inputFields...)
233+
133 content := lipgloss.JoinVertical(
234 content := lipgloss.JoinVertical(
134 lipgloss.Left,
235 lipgloss.Left,
135− title,
236+ elements...,
136− explanation,
137− inputField,
138 )
237 )
139
238
140 return baseStyle.Padding(1, 2).
239 return baseStyle.Padding(1, 2).
@@ -147,27 +246,12 @@ func (m ArgumentsDialogCmp) View() string {
147}
246}
148
247
149// SetSize sets the size of the component.
248// SetSize sets the size of the component.
150−func (m *ArgumentsDialogCmp) SetSize(width, height int) {
249+func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
151 m.width = width
250 m.width = width
152 m.height = height
251 m.height = height
153}
252}
154
253
155// Bindings implements layout.Bindings.
254// Bindings implements layout.Bindings.
156−func (m ArgumentsDialogCmp) Bindings() []key.Binding {
255+func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
157 return m.keys.ShortHelp()
256 return m.keys.ShortHelp()
158−}
257+}
159−
160−// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
161−type CloseArgumentsDialogMsg struct {
162− Submit bool
163− CommandID string
164− Content string
165− Arguments string
166−}
167−
168−// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
169−type ShowArgumentsDialogMsg struct {
170− CommandID string
171− Content string
172−}
173−
internal/tui/components/dialog/custom_commands.go+24−4
@@ -4,6 +4,7 @@ import (
4 "fmt"
4 "fmt"
5 "os"
5 "os"
6 "path/filepath"
6 "path/filepath"
7+ "regexp"
7 "strings"
8 "strings"
8
9
9 tea "github.com/charmbracelet/bubbletea"
10 tea "github.com/charmbracelet/bubbletea"
@@ -17,6 +18,9 @@ const (
17 ProjectCommandPrefix = "project:"
18 ProjectCommandPrefix = "project:"
18)
19)
19
20
21+// namedArgPattern is a regex pattern to find named arguments in the format $NAME
22+var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
23+
20// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
24// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
21func LoadCustomCommands() ([]Command, error) {
25func LoadCustomCommands() ([]Command, error) {
22 cfg := config.Get()
26 cfg := config.Get()
@@ -133,18 +137,33 @@ func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
133 Handler: func(cmd Command) tea.Cmd {
137 Handler: func(cmd Command) tea.Cmd {
134 commandContent := string(content)
138 commandContent := string(content)
135
139
136− // Check if the command contains $ARGUMENTS placeholder
140+ // Check for named arguments
137− if strings.Contains(commandContent, "$ARGUMENTS") {
141+ matches := namedArgPattern.FindAllStringSubmatch(commandContent, -1)
142+ if len(matches) > 0 {
143+ // Extract unique argument names
144+ argNames := make([]string, 0)
145+ argMap := make(map[string]bool)
146+
147+ for _, match := range matches {
148+ argName := match[1] // Group 1 is the name without $
149+ if !argMap[argName] {
150+ argMap[argName] = true
151+ argNames = append(argNames, argName)
152+ }
153+ }
154+
138− // Show arguments dialog
155+ // Show multi-arguments dialog for all named arguments
139− return util.CmdHandler(ShowArgumentsDialogMsg{
156+ return util.CmdHandler(ShowMultiArgumentsDialogMsg{
140 CommandID: cmd.ID,
157 CommandID: cmd.ID,
141 Content: commandContent,
158 Content: commandContent,
159+ ArgNames: argNames,
142 })
160 })
143 }
161 }
144
162
145 // No arguments needed, run command directly
163 // No arguments needed, run command directly
146 return util.CmdHandler(CommandRunCustomMsg{
164 return util.CmdHandler(CommandRunCustomMsg{
147 Content: commandContent,
165 Content: commandContent,
166+ Args: nil, // No arguments
148 })
167 })
149 },
168 },
150 }
169 }
@@ -163,4 +182,5 @@ func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
163// CommandRunCustomMsg is sent when a custom command is executed
182// CommandRunCustomMsg is sent when a custom command is executed
164type CommandRunCustomMsg struct {
183type CommandRunCustomMsg struct {
165 Content string
184 Content string
185+ Args map[string]string // Map of argument names to values
166}
186}
internal/tui/components/dialog/custom_commands_test.gonew file+106
@@ -0,0 +1,106 @@
1+package dialog
2+
3+import (
4+ "testing"
5+ "regexp"
6+)
7+
8+func TestNamedArgPattern(t *testing.T) {
9+ testCases := []struct {
10+ input string
11+ expected []string
12+ }{
13+ {
14+ input: "This is a test with $ARGUMENTS placeholder",
15+ expected: []string{"ARGUMENTS"},
16+ },
17+ {
18+ input: "This is a test with $FOO and $BAR placeholders",
19+ expected: []string{"FOO", "BAR"},
20+ },
21+ {
22+ input: "This is a test with $FOO_BAR and $BAZ123 placeholders",
23+ expected: []string{"FOO_BAR", "BAZ123"},
24+ },
25+ {
26+ input: "This is a test with no placeholders",
27+ expected: []string{},
28+ },
29+ {
30+ input: "This is a test with $FOO appearing twice: $FOO",
31+ expected: []string{"FOO"},
32+ },
33+ {
34+ input: "This is a test with $1INVALID placeholder",
35+ expected: []string{},
36+ },
37+ }
38+
39+ for _, tc := range testCases {
40+ matches := namedArgPattern.FindAllStringSubmatch(tc.input, -1)
41+
42+ // Extract unique argument names
43+ argNames := make([]string, 0)
44+ argMap := make(map[string]bool)
45+
46+ for _, match := range matches {
47+ argName := match[1] // Group 1 is the name without $
48+ if !argMap[argName] {
49+ argMap[argName] = true
50+ argNames = append(argNames, argName)
51+ }
52+ }
53+
54+ // Check if we got the expected number of arguments
55+ if len(argNames) != len(tc.expected) {
56+ t.Errorf("Expected %d arguments, got %d for input: %s", len(tc.expected), len(argNames), tc.input)
57+ continue
58+ }
59+
60+ // Check if we got the expected argument names
61+ for _, expectedArg := range tc.expected {
62+ found := false
63+ for _, actualArg := range argNames {
64+ if actualArg == expectedArg {
65+ found = true
66+ break
67+ }
68+ }
69+ if !found {
70+ t.Errorf("Expected argument %s not found in %v for input: %s", expectedArg, argNames, tc.input)
71+ }
72+ }
73+ }
74+}
75+
76+func TestRegexPattern(t *testing.T) {
77+ pattern := regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
78+
79+ validMatches := []string{
80+ "$FOO",
81+ "$BAR",
82+ "$FOO_BAR",
83+ "$BAZ123",
84+ "$ARGUMENTS",
85+ }
86+
87+ invalidMatches := []string{
88+ "$foo",
89+ "$1BAR",
90+ "$_FOO",
91+ "FOO",
92+ "$",
93+ }
94+
95+ for _, valid := range validMatches {
96+ if !pattern.MatchString(valid) {
97+ t.Errorf("Expected %s to match, but it didn't", valid)
98+ }
99+ }
100+
101+ for _, invalid := range invalidMatches {
102+ if pattern.MatchString(invalid) {
103+ t.Errorf("Expected %s not to match, but it did", invalid)
104+ }
105+ }
106+}
internal/tui/page/chat.go+13−1
@@ -2,6 +2,7 @@ package page
2
2
3import (
3import (
4 "context"
4 "context"
5+ "strings"
5
6
6 "github.com/charmbracelet/bubbles/key"
7 "github.com/charmbracelet/bubbles/key"
7 tea "github.com/charmbracelet/bubbletea"
8 tea "github.com/charmbracelet/bubbletea"
@@ -63,8 +64,19 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
63 if p.app.CoderAgent.IsBusy() {
64 if p.app.CoderAgent.IsBusy() {
64 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
65 return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
65 }
66 }
67+
68+ // Process the command content with arguments if any
69+ content := msg.Content
70+ if msg.Args != nil {
71+ // Replace all named arguments with their values
72+ for name, value := range msg.Args {
73+ placeholder := "$" + name
74+ content = strings.ReplaceAll(content, placeholder, value)
75+ }
76+ }
77+
66 // Handle custom command execution
78 // Handle custom command execution
67− cmd := p.sendMessage(msg.Content, nil)
79+ cmd := p.sendMessage(content, nil)
68 if cmd != nil {
80 if cmd != nil {
69 return p, cmd
81 return p, cmd
70 }
82 }
internal/tui/tui.go+32−26
@@ -133,8 +133,8 @@ type appModel struct {
133 showThemeDialog bool
133 showThemeDialog bool
134 themeDialog dialog.ThemeDialog
134 themeDialog dialog.ThemeDialog
135
135
136− showArgumentsDialog bool
136+ showMultiArgumentsDialog bool
137− argumentsDialog dialog.ArgumentsDialogCmp
137+ multiArgumentsDialog dialog.MultiArgumentsDialogCmp
138
138
139 isCompacting bool
139 isCompacting bool
140 compactingMessage string
140 compactingMessage string
@@ -214,11 +214,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
214
214
215 a.initDialog.SetSize(msg.Width, msg.Height)
215 a.initDialog.SetSize(msg.Width, msg.Height)
216
216
217− if a.showArgumentsDialog {
217+ if a.showMultiArgumentsDialog {
218− a.argumentsDialog.SetSize(msg.Width, msg.Height)
218+ a.multiArgumentsDialog.SetSize(msg.Width, msg.Height)
219− args, argsCmd := a.argumentsDialog.Update(msg)
219+ args, argsCmd := a.multiArgumentsDialog.Update(msg)
220− a.argumentsDialog = args.(dialog.ArgumentsDialogCmp)
220+ a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
221− cmds = append(cmds, argsCmd, a.argumentsDialog.Init())
221+ cmds = append(cmds, argsCmd, a.multiArgumentsDialog.Init())
222 }
222 }
223
223
224 return a, tea.Batch(cmds...)
224 return a, tea.Batch(cmds...)
@@ -438,33 +438,39 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
438 }
438 }
439 return a, util.ReportInfo("Command selected: " + msg.Command.Title)
439 return a, util.ReportInfo("Command selected: " + msg.Command.Title)
440
440
441− case dialog.ShowArgumentsDialogMsg:
441+ case dialog.ShowMultiArgumentsDialogMsg:
442− // Show arguments dialog
442+ // Show multi-arguments dialog
443− a.argumentsDialog = dialog.NewArgumentsDialogCmp(msg.CommandID, msg.Content)
443+ a.multiArgumentsDialog = dialog.NewMultiArgumentsDialogCmp(msg.CommandID, msg.Content, msg.ArgNames)
444− a.showArgumentsDialog = true
444+ a.showMultiArgumentsDialog = true
445− return a, a.argumentsDialog.Init()
445+ return a, a.multiArgumentsDialog.Init()
446
446
447− case dialog.CloseArgumentsDialogMsg:
447+ case dialog.CloseMultiArgumentsDialogMsg:
448− // Close arguments dialog
448+ // Close multi-arguments dialog
449− a.showArgumentsDialog = false
449+ a.showMultiArgumentsDialog = false
450
450
451− // If submitted, replace $ARGUMENTS and run the command
451+ // If submitted, replace all named arguments and run the command
452 if msg.Submit {
452 if msg.Submit {
453− // Replace $ARGUMENTS with the provided arguments
453+ content := msg.Content
454− content := strings.ReplaceAll(msg.Content, "$ARGUMENTS", msg.Arguments)
454+
455+ // Replace each named argument with its value
456+ for name, value := range msg.Args {
457+ placeholder := "$" + name
458+ content = strings.ReplaceAll(content, placeholder, value)
459+ }
455
460
456 // Execute the command with arguments
461 // Execute the command with arguments
457 return a, util.CmdHandler(dialog.CommandRunCustomMsg{
462 return a, util.CmdHandler(dialog.CommandRunCustomMsg{
458 Content: content,
463 Content: content,
464+ Args: msg.Args,
459 })
465 })
460 }
466 }
461 return a, nil
467 return a, nil
462
468
463 case tea.KeyMsg:
469 case tea.KeyMsg:
464− // If arguments dialog is open, let it handle the key press first
470+ // If multi-arguments dialog is open, let it handle the key press first
465− if a.showArgumentsDialog {
471+ if a.showMultiArgumentsDialog {
466− args, cmd := a.argumentsDialog.Update(msg)
472+ args, cmd := a.multiArgumentsDialog.Update(msg)
467− a.argumentsDialog = args.(dialog.ArgumentsDialogCmp)
473+ a.multiArgumentsDialog = args.(dialog.MultiArgumentsDialogCmp)
468 return a, cmd
474 return a, cmd
469 }
475 }
470
476
@@ -488,8 +494,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
488 if a.showModelDialog {
494 if a.showModelDialog {
489 a.showModelDialog = false
495 a.showModelDialog = false
490 }
496 }
491− if a.showArgumentsDialog {
497+ if a.showMultiArgumentsDialog {
492− a.showArgumentsDialog = false
498+ a.showMultiArgumentsDialog = false
493 }
499 }
494 return a, nil
500 return a, nil
495 case key.Matches(msg, keys.SwitchSession):
501 case key.Matches(msg, keys.SwitchSession):
@@ -898,8 +904,8 @@ func (a appModel) View() string {
898 )
904 )
899 }
905 }
900
906
901− if a.showArgumentsDialog {
907+ if a.showMultiArgumentsDialog {
902− overlay := a.argumentsDialog.View()
908+ overlay := a.multiArgumentsDialog.View()
903 row := lipgloss.Height(appView) / 2
909 row := lipgloss.Height(appView) / 2
904 row -= lipgloss.Height(overlay) / 2
910 row -= lipgloss.Height(overlay) / 2
905 col := lipgloss.Width(appView) / 2
911 col := lipgloss.Width(appView) / 2