diff options
| author | Dax <[email protected]> | 2025-08-22 17:04:28 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-22 17:04:28 -0400 |
| commit | 133fe41cd5fb0ec3dc03a90e58526408297878fd (patch) | |
| tree | 82aac357a61fe9e7a6802974f6c7cf169a9f22a8 /packages/tui/internal | |
| parent | 74c1085103e130cb07522e3ad725f17eca3e5832 (diff) | |
| download | opencode-133fe41cd5fb0ec3dc03a90e58526408297878fd.tar.gz opencode-133fe41cd5fb0ec3dc03a90e58526408297878fd.zip | |
slash commands (#2157)
Co-authored-by: adamdotdevin <[email protected]>
Diffstat (limited to 'packages/tui/internal')
| -rw-r--r-- | packages/tui/internal/app/app.go | 43 | ||||
| -rw-r--r-- | packages/tui/internal/commands/command.go | 14 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/editor.go | 28 | ||||
| -rw-r--r-- | packages/tui/internal/components/chat/messages.go | 4 | ||||
| -rw-r--r-- | packages/tui/internal/tui/tui.go | 18 |
5 files changed, 104 insertions, 3 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 0c703c959..ecf95ff98 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -84,6 +84,10 @@ type SendPrompt = Prompt type SendShell = struct { Command string } +type SendCommand = struct { + Command string + Args string +} type SetEditorContentMsg struct { Text string } @@ -183,6 +187,11 @@ func New( slog.Debug("Loaded config", "config", configInfo) + customCommands, err := httpClient.Command.List(ctx) + if err != nil { + return nil, err + } + app := &App{ Info: appInfo, Agents: agents, @@ -194,7 +203,7 @@ func New( AgentIndex: agentIndex, Session: &opencode.Session{}, Messages: []Message{}, - Commands: commands.LoadFromConfig(configInfo), + Commands: commands.LoadFromConfig(configInfo, *customCommands), InitialModel: initialModel, InitialPrompt: initialPrompt, InitialAgent: initialAgent, @@ -793,6 +802,38 @@ func (a *App) SendPrompt(ctx context.Context, prompt Prompt) (*App, tea.Cmd) { return a, tea.Batch(cmds...) } +func (a *App) SendCommand(ctx context.Context, command string, args string) (*App, tea.Cmd) { + var cmds []tea.Cmd + if a.Session.ID == "" { + session, err := a.CreateSession(ctx) + if err != nil { + return a, toast.NewErrorToast(err.Error()) + } + a.Session = session + cmds = append(cmds, util.CmdHandler(SessionCreatedMsg{Session: session})) + } + + cmds = append(cmds, func() tea.Msg { + _, err := a.Client.Session.Command( + context.Background(), + a.Session.ID, + opencode.SessionCommandParams{ + Command: opencode.F(command), + Arguments: opencode.F(args), + }, + ) + if err != nil { + slog.Error("Failed to execute command", "error", err) + return toast.NewErrorToast("Failed to execute command") + } + return nil + }) + + // The actual response will come through SSE + // For now, just return success + return a, tea.Batch(cmds...) +} + func (a *App) SendShell(ctx context.Context, command string) (*App, tea.Cmd) { var cmds []tea.Cmd if a.Session.ID == "" { diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go index bd5d61b95..3a5287ca3 100644 --- a/packages/tui/internal/commands/command.go +++ b/packages/tui/internal/commands/command.go @@ -31,6 +31,7 @@ type Command struct { Description string Keybindings []Keybinding Trigger []string + Custom bool } func (c Command) Keys() []string { @@ -96,6 +97,7 @@ func (r CommandRegistry) Sorted() []Command { }) return commands } + func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command { var matched []Command for _, command := range r.Sorted() { @@ -182,7 +184,7 @@ func parseBindings(bindings ...string) []Keybinding { return parsedBindings } -func LoadFromConfig(config *opencode.Config) CommandRegistry { +func LoadFromConfig(config *opencode.Config, customCommands []opencode.Command) CommandRegistry { defaults := []Command{ { Name: AppHelpCommand, @@ -400,6 +402,16 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { } registry[command.Name] = command } + for _, command := range customCommands { + registry[CommandName(command.Name)] = Command{ + Name: CommandName(command.Name), + Description: command.Description, + Trigger: []string{command.Name}, + Keybindings: []Keybinding{}, + Custom: true, + } + } + slog.Info("Loaded commands", "commands", registry) return registry } diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go index c5ecdc21d..0c52ca84c 100644 --- a/packages/tui/internal/components/chat/editor.go +++ b/packages/tui/internal/components/chat/editor.go @@ -224,10 +224,17 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case dialog.CompletionSelectedMsg: switch msg.Item.ProviderID { case "commands": - commandName := strings.TrimPrefix(msg.Item.Value, "/") + command := msg.Item.RawData.(commands.Command) + if command.Custom { + m.SetValue("/" + command.PrimaryTrigger() + " ") + return m, nil + } + updated, cmd := m.Clear() m = updated.(*editorComponent) cmds = append(cmds, cmd) + + commandName := strings.TrimPrefix(msg.Item.Value, "/") cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]))) return m, tea.Batch(cmds...) case "files": @@ -481,6 +488,25 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) { } var cmds []tea.Cmd + if strings.HasPrefix(value, "/") { + value = value[1:] + commandName := strings.Split(value, " ")[0] + command := m.app.Commands[commands.CommandName(commandName)] + if command.Custom { + args := strings.TrimPrefix(value, command.PrimaryTrigger()+" ") + cmds = append( + cmds, + util.CmdHandler(app.SendCommand{Command: string(command.Name), Args: args}), + ) + + updated, cmd := m.Clear() + m = updated.(*editorComponent) + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) + } + } + attachments := m.textarea.GetAttachments() prompt := app.Prompt{Text: value, Attachments: attachments} diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go index 97c529721..a299d65af 100644 --- a/packages/tui/internal/components/chat/messages.go +++ b/packages/tui/internal/components/chat/messages.go @@ -174,6 +174,10 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.GotoBottom() m.tail = true return m, nil + case app.SendCommand: + m.viewport.GotoBottom() + m.tail = true + return m, nil case dialog.ThemeSelectedMsg: m.cache.Clear() m.loading = true diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 26a1ba25a..f7ce7982b 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -408,6 +408,24 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.app, cmd = a.app.SendPrompt(context.Background(), msg) cmds = append(cmds, cmd) } + case app.SendCommand: + // If we're in a child session, switch back to parent before sending prompt + if a.app.Session.ParentID != "" { + parentSession, err := a.app.Client.Session.Get(context.Background(), a.app.Session.ParentID) + if err != nil { + slog.Error("Failed to get parent session", "error", err) + return a, toast.NewErrorToast("Failed to get parent session") + } + a.app.Session = parentSession + a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args) + cmds = append(cmds, tea.Sequence( + util.CmdHandler(app.SessionSelectedMsg(parentSession)), + cmd, + )) + } else { + a.app, cmd = a.app.SendCommand(context.Background(), msg.Command, msg.Args) + cmds = append(cmds, cmd) + } case app.SendShell: // If we're in a child session, switch back to parent before sending prompt if a.app.Session.ParentID != "" { |
