mirror of
https://github.com/sst/opencode.git
synced 2025-08-26 16:04:07 +00:00
feat(tui): custom commands
This commit is contained in:
parent
2407b33b1b
commit
fdfb54aea5
21 changed files with 526 additions and 26 deletions
|
@ -1,3 +1,7 @@
|
|||
---
|
||||
description: hello world
|
||||
---
|
||||
|
||||
hey there $ARGUMENTS
|
||||
|
||||
!`ls`
|
||||
|
|
15
logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json
Normal file
15
logs/.496fc674ed58d31f8b883da41cc2adb4564aad58-audit.json
Normal file
|
@ -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"
|
||||
}
|
6
logs/mcp-puppeteer-2025-08-22.log
Normal file
6
logs/mcp-puppeteer-2025-08-22.log
Normal file
|
@ -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"}
|
|
@ -3,13 +3,17 @@ import { App } from "../app/app"
|
|||
import { Config } from "../config/config"
|
||||
|
||||
export namespace Command {
|
||||
export const Info = z.object({
|
||||
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<typeof Info>
|
||||
|
||||
const state = App.state("command", async () => {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -70,6 +70,16 @@ Methods:
|
|||
|
||||
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Command
|
||||
|
||||
Response Types:
|
||||
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>
|
||||
|
||||
Methods:
|
||||
|
||||
- <code title="get /command">client.Command.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#CommandService.List">List</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Command">Command</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
||||
# Session
|
||||
|
||||
Params Types:
|
||||
|
@ -106,6 +116,7 @@ Response Types:
|
|||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ToolStateRunning">ToolStateRunning</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#UserMessage">UserMessage</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>
|
||||
- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessagesResponse">SessionMessagesResponse</a>
|
||||
|
||||
|
@ -118,6 +129,7 @@ Methods:
|
|||
- <code title="post /session/{id}/abort">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Abort">Abort</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/message">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Chat">Chat</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatParams">SessionChatParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionChatResponse">SessionChatResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/children">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Children">Children</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) ([]<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/command">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Command">Command</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandParams">SessionCommandParams</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionCommandResponse">SessionCommandResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="post /session/{id}/init">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Init">Init</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, body <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionInitParams">SessionInitParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
- <code title="get /session/{id}/message/{messageID}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Message">Message</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, id <a href="https://pkg.go.dev/builtin#string">string</a>, messageID <a href="https://pkg.go.dev/builtin#string">string</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionMessageResponse">SessionMessageResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
|
||||
|
|
|
@ -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...)
|
||||
|
||||
|
|
67
packages/sdk/go/command.go
Normal file
67
packages/sdk/go/command.go
Normal file
|
@ -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
|
||||
}
|
36
packages/sdk/go/command_test.go
Normal file
36
packages/sdk/go/command_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ type Config struct {
|
|||
Autoshare bool `json:"autoshare"`
|
||||
// Automatically update to the latest version
|
||||
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:"-"`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<ThrowOnError extends boolean = false>(options: Options<SessionCommandData, ThrowOnError>) {
|
||||
return (options.client ?? this._client).post<SessionCommandResponses, unknown, ThrowOnError>({
|
||||
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<ThrowOnError extends boolean = false>(options?: Options<CommandListData, ThrowOnError>) {
|
||||
return (options?.client ?? this._client).get<CommandListResponses, unknown, ThrowOnError>({
|
||||
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 })
|
||||
|
|
|
@ -585,6 +585,14 @@ export type Config = {
|
|||
*/
|
||||
scroll_speed: number
|
||||
}
|
||||
command?: {
|
||||
[key: string]: {
|
||||
template: string
|
||||
description?: string
|
||||
agent?: string
|
||||
model?: string
|
||||
}
|
||||
}
|
||||
plugin?: Array<string>
|
||||
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<Part>
|
||||
}
|
||||
}
|
||||
|
||||
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<Command>
|
||||
}
|
||||
|
||||
export type CommandListResponse = CommandListResponses[keyof CommandListResponses]
|
||||
|
||||
export type ConfigProvidersData = {
|
||||
body?: never
|
||||
path?: never
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 == "" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue