summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal
diff options
context:
space:
mode:
authorDax <[email protected]>2025-08-22 17:04:28 -0400
committerGitHub <[email protected]>2025-08-22 17:04:28 -0400
commit133fe41cd5fb0ec3dc03a90e58526408297878fd (patch)
tree82aac357a61fe9e7a6802974f6c7cf169a9f22a8 /packages/tui/internal
parent74c1085103e130cb07522e3ad725f17eca3e5832 (diff)
downloadopencode-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.go43
-rw-r--r--packages/tui/internal/commands/command.go14
-rw-r--r--packages/tui/internal/components/chat/editor.go28
-rw-r--r--packages/tui/internal/components/chat/messages.go4
-rw-r--r--packages/tui/internal/tui/tui.go18
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 != "" {