diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts
index a4239651..976e0fb3 100644
--- a/packages/opencode/src/permission/index.ts
+++ b/packages/opencode/src/permission/index.ts
@@ -29,6 +29,10 @@ export namespace Permission {
export const Event = {
Updated: Bus.event("permission.updated", Info),
+ Replied: Bus.event(
+ "permission.replied",
+ z.object({ sessionID: z.string(), permissionID: z.string(), response: z.string() }),
+ ),
}
const state = App.state(
@@ -120,9 +124,19 @@ export namespace Permission {
return
}
match.resolve()
+ Bus.publish(Event.Replied, {
+ sessionID: input.sessionID,
+ permissionID: input.permissionID,
+ response: input.response,
+ })
if (input.response === "always") {
approved[input.sessionID] = approved[input.sessionID] || {}
approved[input.sessionID][input.permissionID] = match.info
+ for (const item of Object.values(pending[input.sessionID])) {
+ if ((item.info.pattern ?? item.info.type) === (match.info.pattern ?? match.info.type)) {
+ respond({ sessionID: item.info.sessionID, permissionID: item.info.id, response: input.response })
+ }
+ }
}
}
diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml
index 471c9026..7b29797c 100644
--- a/packages/sdk/go/.stats.yml
+++ b/packages/sdk/go/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 28
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-3fa00e84a92784c0e12cf47a49cf5ac4eb5556b5b3ad8769ad7b4e7e1bf1b01a.yml
-openapi_spec_hash: 5f98ce812d7feb00e6c2eb7a15dd8887
-config_hash: 7707d73ebbd7ad7042ab70466b39348d
+configured_endpoints: 34
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-2ebd9d5478864042a2e01b4995f42acbc39069fa7fcccd1c2e567366ee6c243d.yml
+openapi_spec_hash: 2a34451b288ea30af1cb61332c417c2a
+config_hash: 11a6f0803eb407367c3f677d3e524c37
diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md
index 0291c776..672460b9 100644
--- a/packages/sdk/go/api.md
+++ b/packages/sdk/go/api.md
@@ -137,4 +137,10 @@ Methods:
Methods:
- client.Tui.AppendPrompt(ctx context.Context, body opencode.TuiAppendPromptParams) (bool, error)
+- client.Tui.ClearPrompt(ctx context.Context) (bool, error)
+- client.Tui.ExecuteCommand(ctx context.Context, body opencode.TuiExecuteCommandParams) (bool, error)
- client.Tui.OpenHelp(ctx context.Context) (bool, error)
+- client.Tui.OpenModels(ctx context.Context) (bool, error)
+- client.Tui.OpenSessions(ctx context.Context) (bool, error)
+- client.Tui.OpenThemes(ctx context.Context) (bool, error)
+- client.Tui.SubmitPrompt(ctx context.Context) (bool, error)
diff --git a/packages/sdk/go/event.go b/packages/sdk/go/event.go
index 3c08b327..f1627080 100644
--- a/packages/sdk/go/event.go
+++ b/packages/sdk/go/event.go
@@ -55,6 +55,7 @@ type EventListResponse struct {
// [EventListResponseEventMessagePartUpdatedProperties],
// [EventListResponseEventMessagePartRemovedProperties],
// [EventListResponseEventStorageWriteProperties], [Permission],
+ // [EventListResponseEventPermissionRepliedProperties],
// [EventListResponseEventFileEditedProperties],
// [EventListResponseEventSessionUpdatedProperties],
// [EventListResponseEventSessionDeletedProperties],
@@ -100,9 +101,10 @@ func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
// [EventListResponseEventMessagePartUpdated],
// [EventListResponseEventMessagePartRemoved],
// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated],
-// [EventListResponseEventFileEdited], [EventListResponseEventSessionUpdated],
-// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionIdle],
-// [EventListResponseEventSessionError], [EventListResponseEventServerConnected],
+// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
+// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
+// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
+// [EventListResponseEventServerConnected],
// [EventListResponseEventFileWatcherUpdated],
// [EventListResponseEventIdeInstalled].
func (r EventListResponse) AsUnion() EventListResponseUnion {
@@ -115,9 +117,10 @@ func (r EventListResponse) AsUnion() EventListResponseUnion {
// [EventListResponseEventMessagePartUpdated],
// [EventListResponseEventMessagePartRemoved],
// [EventListResponseEventStorageWrite], [EventListResponseEventPermissionUpdated],
-// [EventListResponseEventFileEdited], [EventListResponseEventSessionUpdated],
-// [EventListResponseEventSessionDeleted], [EventListResponseEventSessionIdle],
-// [EventListResponseEventSessionError], [EventListResponseEventServerConnected],
+// [EventListResponseEventPermissionReplied], [EventListResponseEventFileEdited],
+// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
+// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
+// [EventListResponseEventServerConnected],
// [EventListResponseEventFileWatcherUpdated] or
// [EventListResponseEventIdeInstalled].
type EventListResponseUnion interface {
@@ -168,6 +171,11 @@ func init() {
Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}),
DiscriminatorValue: "permission.updated",
},
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventPermissionReplied{}),
+ DiscriminatorValue: "permission.replied",
+ },
apijson.UnionVariant{
TypeFilter: gjson.JSON,
Type: reflect.TypeOf(EventListResponseEventFileEdited{}),
@@ -680,6 +688,70 @@ func (r EventListResponseEventPermissionUpdatedType) IsKnown() bool {
return false
}
+type EventListResponseEventPermissionReplied struct {
+ Properties EventListResponseEventPermissionRepliedProperties `json:"properties,required"`
+ Type EventListResponseEventPermissionRepliedType `json:"type,required"`
+ JSON eventListResponseEventPermissionRepliedJSON `json:"-"`
+}
+
+// eventListResponseEventPermissionRepliedJSON contains the JSON metadata for the
+// struct [EventListResponseEventPermissionReplied]
+type eventListResponseEventPermissionRepliedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventPermissionReplied) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventPermissionRepliedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventPermissionReplied) implementsEventListResponse() {}
+
+type EventListResponseEventPermissionRepliedProperties struct {
+ PermissionID string `json:"permissionID,required"`
+ Response string `json:"response,required"`
+ SessionID string `json:"sessionID,required"`
+ JSON eventListResponseEventPermissionRepliedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventPermissionRepliedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventPermissionRepliedProperties]
+type eventListResponseEventPermissionRepliedPropertiesJSON struct {
+ PermissionID apijson.Field
+ Response apijson.Field
+ SessionID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventPermissionRepliedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventPermissionRepliedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventPermissionRepliedType string
+
+const (
+ EventListResponseEventPermissionRepliedTypePermissionReplied EventListResponseEventPermissionRepliedType = "permission.replied"
+)
+
+func (r EventListResponseEventPermissionRepliedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventPermissionRepliedTypePermissionReplied:
+ return true
+ }
+ return false
+}
+
type EventListResponseEventFileEdited struct {
Properties EventListResponseEventFileEditedProperties `json:"properties,required"`
Type EventListResponseEventFileEditedType `json:"type,required"`
@@ -1303,6 +1375,7 @@ const (
EventListResponseTypeMessagePartRemoved EventListResponseType = "message.part.removed"
EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
+ EventListResponseTypePermissionReplied EventListResponseType = "permission.replied"
EventListResponseTypeFileEdited EventListResponseType = "file.edited"
EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
@@ -1315,7 +1388,7 @@ const (
func (r EventListResponseType) IsKnown() bool {
switch r {
- case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled:
+ case EventListResponseTypeInstallationUpdated, EventListResponseTypeLspClientDiagnostics, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeMessagePartRemoved, EventListResponseTypeStorageWrite, EventListResponseTypePermissionUpdated, EventListResponseTypePermissionReplied, EventListResponseTypeFileEdited, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeServerConnected, EventListResponseTypeFileWatcherUpdated, EventListResponseTypeIdeInstalled:
return true
}
return false
diff --git a/packages/sdk/go/tui.go b/packages/sdk/go/tui.go
index d5243599..30657890 100644
--- a/packages/sdk/go/tui.go
+++ b/packages/sdk/go/tui.go
@@ -39,6 +39,22 @@ func (r *TuiService) AppendPrompt(ctx context.Context, body TuiAppendPromptParam
return
}
+// Clear the prompt
+func (r *TuiService) ClearPrompt(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "tui/clear-prompt"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// Execute a TUI command (e.g. switch_mode)
+func (r *TuiService) ExecuteCommand(ctx context.Context, body TuiExecuteCommandParams, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "tui/execute-command"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
// Open the help dialog
func (r *TuiService) OpenHelp(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
opts = append(r.Options[:], opts...)
@@ -47,6 +63,38 @@ func (r *TuiService) OpenHelp(ctx context.Context, opts ...option.RequestOption)
return
}
+// Open the model dialog
+func (r *TuiService) OpenModels(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "tui/open-models"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// Open the session dialog
+func (r *TuiService) OpenSessions(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "tui/open-sessions"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// Open the theme dialog
+func (r *TuiService) OpenThemes(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "tui/open-themes"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// Submit the prompt
+func (r *TuiService) SubmitPrompt(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "tui/submit-prompt"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
type TuiAppendPromptParams struct {
Text param.Field[string] `json:"text,required"`
}
@@ -54,3 +102,11 @@ type TuiAppendPromptParams struct {
func (r TuiAppendPromptParams) MarshalJSON() (data []byte, err error) {
return apijson.MarshalRoot(r)
}
+
+type TuiExecuteCommandParams struct {
+ Command param.Field[string] `json:"command,required"`
+}
+
+func (r TuiExecuteCommandParams) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
diff --git a/packages/sdk/go/tui_test.go b/packages/sdk/go/tui_test.go
index 5283f37c..f3260aaf 100644
--- a/packages/sdk/go/tui_test.go
+++ b/packages/sdk/go/tui_test.go
@@ -37,6 +37,52 @@ func TestTuiAppendPrompt(t *testing.T) {
}
}
+func TestTuiClearPrompt(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.Tui.ClearPrompt(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())
+ }
+}
+
+func TestTuiExecuteCommand(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.Tui.ExecuteCommand(context.TODO(), opencode.TuiExecuteCommandParams{
+ Command: opencode.F("command"),
+ })
+ 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 TestTuiOpenHelp(t *testing.T) {
t.Skip("skipped: tests are disabled for the time being")
baseURL := "http://localhost:4010"
@@ -58,3 +104,91 @@ func TestTuiOpenHelp(t *testing.T) {
t.Fatalf("err should be nil: %s", err.Error())
}
}
+
+func TestTuiOpenModels(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.Tui.OpenModels(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())
+ }
+}
+
+func TestTuiOpenSessions(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.Tui.OpenSessions(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())
+ }
+}
+
+func TestTuiOpenThemes(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.Tui.OpenThemes(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())
+ }
+}
+
+func TestTuiSubmitPrompt(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.Tui.SubmitPrompt(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/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index db8e7cf6..a4e861d6 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -200,7 +200,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventPermissionUpdated:
m.tail = true
return m, m.renderView()
- case app.PermissionRespondedToMsg:
+ case opencode.EventListResponseEventPermissionReplied:
m.tail = true
return m, m.renderView()
case renderCompleteMsg:
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 3d08c2af..f108971d 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -143,7 +143,7 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return toast.NewErrorToast("Failed to respond to permission request")
}
slog.Debug("Responded to permission request", "response", resp)
- return app.PermissionRespondedToMsg{Response: response}
+ return nil
}
}
}
@@ -522,8 +522,21 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
slog.Debug("permission updated", "session", msg.Properties.SessionID, "permission", msg.Properties.ID)
a.app.Permissions = append(a.app.Permissions, msg.Properties)
a.app.CurrentPermission = a.app.Permissions[0]
- cmds = append(cmds, toast.NewInfoToast(msg.Properties.Title, toast.WithTitle("Permission requested")))
a.editor.Blur()
+ case opencode.EventListResponseEventPermissionReplied:
+ index := slices.IndexFunc(a.app.Permissions, func(p opencode.Permission) bool {
+ return p.ID == msg.Properties.PermissionID
+ })
+ if index > -1 {
+ a.app.Permissions = append(a.app.Permissions[:index], a.app.Permissions[index+1:]...)
+ }
+ if a.app.CurrentPermission.ID == msg.Properties.PermissionID {
+ if len(a.app.Permissions) > 0 {
+ a.app.CurrentPermission = a.app.Permissions[0]
+ } else {
+ a.app.CurrentPermission = opencode.Permission{}
+ }
+ }
case opencode.EventListResponseEventSessionError:
switch err := msg.Properties.Error.AsUnion().(type) {
case nil: