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: