diff --git a/.opencode/command/hello.md b/.opencode/command/hello.md index ed95ef45..4484b514 100644 --- a/.opencode/command/hello.md +++ b/.opencode/command/hello.md @@ -1,3 +1,7 @@ +--- +description: hello world +--- + hey there $ARGUMENTS !`ls` diff --git a/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json b/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json new file mode 100644 index 00000000..208b3dd9 --- /dev/null +++ b/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json @@ -0,0 +1,15 @@ +{ + "keep": { + "days": true, + "amount": 14 + }, + "auditLog": "/Users/adam/code/opencode/dev/logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json", + "files": [ + { + "date": 1755891797740, + "name": "/Users/adam/code/opencode/dev/logs/mcp-puppeteer-2025-08-22.log", + "hash": "dd9b1f2e98b661ba2f56b91dd9afbdb25e50adbdd52ed1b0eef1d2045235d17c" + } + ], + "hashType": "sha256" +} \ No newline at end of file diff --git a/logs/mcp-puppeteer-2025-08-22.log b/logs/mcp-puppeteer-2025-08-22.log new file mode 100644 index 00000000..800731d8 --- /dev/null +++ b/logs/mcp-puppeteer-2025-08-22.log @@ -0,0 +1,6 @@ +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:43:17.765"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:43:17.766"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:46:45.539"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:46:45.540"} +{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-08-22 14:53:08.159"} +{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-08-22 14:53:08.160"} diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 90f8d845..97dd36a0 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -3,13 +3,17 @@ import { App } from "../app/app" import { Config } from "../config/config" export namespace Command { - export const Info = z.object({ - name: z.string(), - description: z.string().optional(), - agent: z.string().optional(), - model: z.string().optional(), - template: z.string(), - }) + export const Info = z + .object({ + name: z.string(), + description: z.string().optional(), + agent: z.string().optional(), + model: z.string().optional(), + template: z.string(), + }) + .openapi({ + ref: "Command", + }) export type Info = z.infer const state = App.state("command", async () => { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index d9bee07a..31951eed 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -21,6 +21,7 @@ import { Permission } from "../permission" import { lazy } from "../util/lazy" import { Agent } from "../agent/agent" import { Auth } from "../auth" +import { Command } from "../command" const ERRORS = { 400: { @@ -632,21 +633,45 @@ export namespace Server { async (c) => { const sessionID = c.req.valid("param").id const body = c.req.valid("json") - const text = body.parts.find((x) => x.type === "text")?.text - if (text?.startsWith("/")) { - const [command, ...args] = text.split(" ") - const msg = await Session.command({ - command: command.slice(1), - arguments: args.join(" "), - sessionID, - ...body, - }) - return c.json(msg) - } const msg = await Session.chat({ ...body, sessionID }) return c.json(msg) }, ) + .post( + "/session/:id/command", + describeRoute({ + description: "Send a new command to a session", + operationId: "session.command", + responses: { + 200: { + description: "Created message", + content: { + "application/json": { + schema: resolver( + z.object({ + info: MessageV2.Assistant, + parts: MessageV2.Part.array(), + }), + ), + }, + }, + }, + }, + }), + zValidator( + "param", + z.object({ + id: z.string().openapi({ description: "Session ID" }), + }), + ), + zValidator("json", Session.CommandInput.omit({ sessionID: true })), + async (c) => { + const sessionID = c.req.valid("param").id + const body = c.req.valid("json") + const msg = await Session.command({ ...body, sessionID }) + return c.json(msg) + }, + ) .post( "/session/:id/shell", describeRoute({ @@ -766,6 +791,27 @@ export namespace Server { return c.json(true) }, ) + .get( + "/command", + describeRoute({ + description: "List all commands", + operationId: "command.list", + responses: { + 200: { + description: "List of commands", + content: { + "application/json": { + schema: resolver(Command.Info.array()), + }, + }, + }, + }, + }), + async (c) => { + const commands = await Command.list() + return c.json(commands) + }, + ) .get( "/config/providers", describeRoute({ diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index 5f222f03..149b4c62 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 39 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-be3e40e0bf7dde2bb15ff82d5d104418fb47fe335808a1aa6468b0be2210a88f.yml -openapi_spec_hash: c1bbb3ebd807656bd9f31a618077e76b -config_hash: eab3723c4c2232a6ba1821151259d6da +configured_endpoints: 41 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d5200eaa145f567a58daa78941ab1141dd63f5f0cfe1596d5c9ecf12d34fea35.yml +openapi_spec_hash: abeb66291dc158f2cdc90bf9945e283e +config_hash: fb625e876313a9f8f31532348fa91f59 diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md index 5accfcb5..2f9eadb6 100644 --- a/packages/sdk/go/api.md +++ b/packages/sdk/go/api.md @@ -70,6 +70,16 @@ Methods: - client.Config.Get(ctx context.Context) (opencode.Config, error) +# Command + +Response Types: + +- opencode.Command + +Methods: + +- client.Command.List(ctx context.Context) ([]opencode.Command, error) + # Session Params Types: @@ -106,6 +116,7 @@ Response Types: - opencode.ToolStateRunning - opencode.UserMessage - opencode.SessionChatResponse +- opencode.SessionCommandResponse - opencode.SessionMessageResponse - opencode.SessionMessagesResponse @@ -118,6 +129,7 @@ Methods: - client.Session.Abort(ctx context.Context, id string) (bool, error) - client.Session.Chat(ctx context.Context, id string, body opencode.SessionChatParams) (opencode.SessionChatResponse, error) - client.Session.Children(ctx context.Context, id string) ([]opencode.Session, error) +- client.Session.Command(ctx context.Context, id string, body opencode.SessionCommandParams) (opencode.SessionCommandResponse, error) - client.Session.Get(ctx context.Context, id string) (opencode.Session, error) - client.Session.Init(ctx context.Context, id string, body opencode.SessionInitParams) (bool, error) - client.Session.Message(ctx context.Context, id string, messageID string) (opencode.SessionMessageResponse, error) diff --git a/packages/sdk/go/client.go b/packages/sdk/go/client.go index 6baf21a8..286408ab 100644 --- a/packages/sdk/go/client.go +++ b/packages/sdk/go/client.go @@ -21,6 +21,7 @@ type Client struct { Find *FindService File *FileService Config *ConfigService + Command *CommandService Session *SessionService Tui *TuiService } @@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r *Client) { r.Find = NewFindService(opts...) r.File = NewFileService(opts...) r.Config = NewConfigService(opts...) + r.Command = NewCommandService(opts...) r.Session = NewSessionService(opts...) r.Tui = NewTuiService(opts...) diff --git a/packages/sdk/go/command.go b/packages/sdk/go/command.go new file mode 100644 index 00000000..9ca70c3a --- /dev/null +++ b/packages/sdk/go/command.go @@ -0,0 +1,67 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode + +import ( + "context" + "net/http" + + "github.com/sst/opencode-sdk-go/internal/apijson" + "github.com/sst/opencode-sdk-go/internal/requestconfig" + "github.com/sst/opencode-sdk-go/option" +) + +// CommandService contains methods and other services that help with interacting +// with the opencode API. +// +// Note, unlike clients, this service does not read variables from the environment +// automatically. You should not instantiate this service directly, and instead use +// the [NewCommandService] method instead. +type CommandService struct { + Options []option.RequestOption +} + +// NewCommandService generates a new service that applies the given options to each +// request. These options are applied after the parent client's options (if there +// is one), and before any request-specific options. +func NewCommandService(opts ...option.RequestOption) (r *CommandService) { + r = &CommandService{} + r.Options = opts + return +} + +// List all commands +func (r *CommandService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Command, err error) { + opts = append(r.Options[:], opts...) + path := "command" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + +type Command struct { + Name string `json:"name,required"` + Template string `json:"template,required"` + Agent string `json:"agent"` + Description string `json:"description"` + Model string `json:"model"` + JSON commandJSON `json:"-"` +} + +// commandJSON contains the JSON metadata for the struct [Command] +type commandJSON struct { + Name apijson.Field + Template apijson.Field + Agent apijson.Field + Description apijson.Field + Model apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *Command) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r commandJSON) RawJSON() string { + return r.raw +} diff --git a/packages/sdk/go/command_test.go b/packages/sdk/go/command_test.go new file mode 100644 index 00000000..5fd8c37b --- /dev/null +++ b/packages/sdk/go/command_test.go @@ -0,0 +1,36 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package opencode_test + +import ( + "context" + "errors" + "os" + "testing" + + "github.com/sst/opencode-sdk-go" + "github.com/sst/opencode-sdk-go/internal/testutil" + "github.com/sst/opencode-sdk-go/option" +) + +func TestCommandList(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Command.List(context.TODO()) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/packages/sdk/go/config.go b/packages/sdk/go/config.go index aae6e5e2..59db54b9 100644 --- a/packages/sdk/go/config.go +++ b/packages/sdk/go/config.go @@ -49,7 +49,8 @@ type Config struct { // automatically Autoshare bool `json:"autoshare"` // Automatically update to the latest version - Autoupdate bool `json:"autoupdate"` + Autoupdate bool `json:"autoupdate"` + Command map[string]ConfigCommand `json:"command"` // Disable providers that are loaded automatically DisabledProviders []string `json:"disabled_providers"` Experimental ConfigExperimental `json:"experimental"` @@ -94,6 +95,7 @@ type configJSON struct { Agent apijson.Field Autoshare apijson.Field Autoupdate apijson.Field + Command apijson.Field DisabledProviders apijson.Field Experimental apijson.Field Formatter apijson.Field @@ -664,6 +666,32 @@ func (r ConfigAgentPlanPermissionWebfetch) IsKnown() bool { return false } +type ConfigCommand struct { + Template string `json:"template,required"` + Agent string `json:"agent"` + Description string `json:"description"` + Model string `json:"model"` + JSON configCommandJSON `json:"-"` +} + +// configCommandJSON contains the JSON metadata for the struct [ConfigCommand] +type configCommandJSON struct { + Template apijson.Field + Agent apijson.Field + Description apijson.Field + Model apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *ConfigCommand) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r configCommandJSON) RawJSON() string { + return r.raw +} + type ConfigExperimental struct { Hook ConfigExperimentalHook `json:"hook"` JSON configExperimentalJSON `json:"-"` diff --git a/packages/sdk/go/session.go b/packages/sdk/go/session.go index a4ad09e2..237b490d 100644 --- a/packages/sdk/go/session.go +++ b/packages/sdk/go/session.go @@ -114,6 +114,18 @@ func (r *SessionService) Children(ctx context.Context, id string, opts ...option return } +// Send a new command to a session +func (r *SessionService) Command(ctx context.Context, id string, body SessionCommandParams, opts ...option.RequestOption) (res *SessionCommandResponse, err error) { + opts = append(r.Options[:], opts...) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("session/%s/command", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + // Get session func (r *SessionService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) { opts = append(r.Options[:], opts...) @@ -2301,6 +2313,29 @@ func (r sessionChatResponseJSON) RawJSON() string { return r.raw } +type SessionCommandResponse struct { + Info AssistantMessage `json:"info,required"` + Parts []Part `json:"parts,required"` + JSON sessionCommandResponseJSON `json:"-"` +} + +// sessionCommandResponseJSON contains the JSON metadata for the struct +// [SessionCommandResponse] +type sessionCommandResponseJSON struct { + Info apijson.Field + Parts apijson.Field + raw string + ExtraFields map[string]apijson.Field +} + +func (r *SessionCommandResponse) UnmarshalJSON(data []byte) (err error) { + return apijson.UnmarshalRoot(data, r) +} + +func (r sessionCommandResponseJSON) RawJSON() string { + return r.raw +} + type SessionMessageResponse struct { Info Message `json:"info,required"` Parts []Part `json:"parts,required"` @@ -2419,6 +2454,18 @@ func (r SessionChatParamsPartsType) IsKnown() bool { return false } +type SessionCommandParams struct { + Arguments param.Field[string] `json:"arguments,required"` + Command param.Field[string] `json:"command,required"` + Agent param.Field[string] `json:"agent"` + MessageID param.Field[string] `json:"messageID"` + Model param.Field[string] `json:"model"` +} + +func (r SessionCommandParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + type SessionInitParams struct { MessageID param.Field[string] `json:"messageID,required"` ModelID param.Field[string] `json:"modelID,required"` diff --git a/packages/sdk/go/session_test.go b/packages/sdk/go/session_test.go index 58e68dc1..d67be255 100644 --- a/packages/sdk/go/session_test.go +++ b/packages/sdk/go/session_test.go @@ -199,6 +199,38 @@ func TestSessionChildren(t *testing.T) { } } +func TestSessionCommandWithOptionalParams(t *testing.T) { + t.Skip("skipped: tests are disabled for the time being") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := opencode.NewClient( + option.WithBaseURL(baseURL), + ) + _, err := client.Session.Command( + context.TODO(), + "id", + opencode.SessionCommandParams{ + Arguments: opencode.F("arguments"), + Command: opencode.F("command"), + Agent: opencode.F("agent"), + MessageID: opencode.F("msg"), + Model: opencode.F("model"), + }, + ) + if err != nil { + var apierr *opencode.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestSessionGet(t *testing.T) { t.Skip("skipped: tests are disabled for the time being") baseURL := "http://localhost:4010" diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index b5e05540..b00216b8 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -39,6 +39,8 @@ import type { SessionChatResponses, SessionMessageData, SessionMessageResponses, + SessionCommandData, + SessionCommandResponses, SessionShellData, SessionShellResponses, SessionRevertData, @@ -47,6 +49,8 @@ import type { SessionUnrevertResponses, PostSessionByIdPermissionsByPermissionIdData, PostSessionByIdPermissionsByPermissionIdResponses, + CommandListData, + CommandListResponses, ConfigProvidersData, ConfigProvidersResponses, FindTextData, @@ -355,6 +359,20 @@ class Session extends _HeyApiClient { }) } + /** + * Send a new command to a session + */ + public command(options: Options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/command", + ...options, + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + }) + } + /** * Run a shell command */ @@ -394,6 +412,18 @@ class Session extends _HeyApiClient { } } +class Command extends _HeyApiClient { + /** + * List all commands + */ + public list(options?: Options) { + return (options?.client ?? this._client).get({ + url: "/command", + ...options, + }) + } +} + class Find extends _HeyApiClient { /** * Find text in files @@ -592,6 +622,7 @@ export class OpencodeClient extends _HeyApiClient { app = new App({ client: this._client }) config = new Config({ client: this._client }) session = new Session({ client: this._client }) + command = new Command({ client: this._client }) find = new Find({ client: this._client }) file = new File({ client: this._client }) tui = new Tui({ client: this._client }) diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 9919dc41..8e9662ad 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -585,6 +585,14 @@ export type Config = { */ scroll_speed: number } + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + } + } plugin?: Array snapshot?: boolean /** @@ -1110,6 +1118,14 @@ export type AgentPartInput = { } } +export type Command = { + name: string + description?: string + agent?: string + model?: string + template: string +} + export type Symbol = { name: string kind: number @@ -1563,6 +1579,36 @@ export type SessionMessageResponses = { export type SessionMessageResponse = SessionMessageResponses[keyof SessionMessageResponses] +export type SessionCommandData = { + body?: { + messageID?: string + agent?: string + model?: string + arguments: string + command: string + } + path: { + /** + * Session ID + */ + id: string + } + query?: never + url: "/session/{id}/command" +} + +export type SessionCommandResponses = { + /** + * Created message + */ + 200: { + info: AssistantMessage + parts: Array + } +} + +export type SessionCommandResponse = SessionCommandResponses[keyof SessionCommandResponses] + export type SessionShellData = { body?: { agent: string @@ -1648,6 +1694,22 @@ export type PostSessionByIdPermissionsByPermissionIdResponses = { export type PostSessionByIdPermissionsByPermissionIdResponse = PostSessionByIdPermissionsByPermissionIdResponses[keyof PostSessionByIdPermissionsByPermissionIdResponses] +export type CommandListData = { + body?: never + path?: never + query?: never + url: "/command" +} + +export type CommandListResponses = { + /** + * List of commands + */ + 200: Array +} + +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] + export type ConfigProvidersData = { body?: never path?: never diff --git a/packages/sdk/stainless/stainless.yml b/packages/sdk/stainless/stainless.yml index e0c040ec..3dd34a41 100644 --- a/packages/sdk/stainless/stainless.yml +++ b/packages/sdk/stainless/stainless.yml @@ -85,6 +85,12 @@ resources: methods: get: get /config + command: + models: + command: Command + methods: + list: get /command + session: models: session: Session @@ -126,6 +132,7 @@ resources: message: get /session/{id}/message/{messageID} messages: get /session/{id}/message chat: post /session/{id}/message + command: post /session/{id}/command shell: post /session/{id}/shell update: patch /session/{id} revert: post /session/{id}/revert diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index 0c703c95..ecf95ff9 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 bd5d61b9..3a5287ca 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 c5ecdc21..0c52ca84 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 97c52972..a299d65a 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 26a1ba25..f7ce7982 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 != "" {