diff --git a/package.json b/package.json index bfd1976f..0bd1560c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "bun run --conditions=development packages/opencode/src/index.ts", "typecheck": "bun run --filter='*' typecheck", - "stainless": "./scripts/stainless", + "generate": "(cd packages/sdk && ./js/script/generate.ts) && (cd packages/sdk/stainless && ./generate.ts)", "postinstall": "./script/hooks" }, "workspaces": { diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index d4a26689..1a3d42b5 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1120,6 +1120,32 @@ export namespace Server { ), async (c) => c.json(await callTui(c)), ) + .post( + "/tui/show-toast", + describeRoute({ + description: "Show a toast notification in the TUI", + operationId: "tui.showToast", + responses: { + 200: { + description: "Toast notification shown successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + }, + }), + zValidator( + "json", + z.object({ + title: z.string().optional(), + message: z.string(), + variant: z.enum(["info", "success", "warning", "error"]), + }), + ), + async (c) => c.json(await callTui(c)), + ) .route("/tui/control", TuiRoute) .put( "/auth/:id", diff --git a/packages/sdk/go/.stats.yml b/packages/sdk/go/.stats.yml index e3713a1c..6a6c7c1f 100644 --- a/packages/sdk/go/.stats.yml +++ b/packages/sdk/go/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 36 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d0eaf92deaa53a25bbbc3181540ad73ed5a4aec6381ac08d8122e24318e5e455.yml -openapi_spec_hash: 22196d859c0711e564b9538d988abda6 -config_hash: 8d85a768523cff92b85ef06c443d49fa +configured_endpoints: 37 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e438f89b86b573c957866c9b057efacc912e3946be03390e4027e8dfab5b83ba.yml +openapi_spec_hash: 4e9d257b86172e266dc9d72fb04548bc +config_hash: a78225c7474eb9ab8745e72a0cfe6f96 diff --git a/packages/sdk/go/api.md b/packages/sdk/go/api.md index 79a67e42..f48d0687 100644 --- a/packages/sdk/go/api.md +++ b/packages/sdk/go/api.md @@ -147,4 +147,5 @@ Methods: - 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.ShowToast(ctx context.Context, body opencode.TuiShowToastParams) (bool, error) - client.Tui.SubmitPrompt(ctx context.Context) (bool, error) diff --git a/packages/sdk/go/tui.go b/packages/sdk/go/tui.go index 6f36ce54..ab5ed640 100644 --- a/packages/sdk/go/tui.go +++ b/packages/sdk/go/tui.go @@ -87,6 +87,14 @@ func (r *TuiService) OpenThemes(ctx context.Context, opts ...option.RequestOptio return } +// Show a toast notification in the TUI +func (r *TuiService) ShowToast(ctx context.Context, body TuiShowToastParams, opts ...option.RequestOption) (res *bool, err error) { + opts = append(r.Options[:], opts...) + path := "tui/show-toast" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &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...) @@ -110,3 +118,30 @@ type TuiExecuteCommandParams struct { func (r TuiExecuteCommandParams) MarshalJSON() (data []byte, err error) { return apijson.MarshalRoot(r) } + +type TuiShowToastParams struct { + Message param.Field[string] `json:"message,required"` + Variant param.Field[TuiShowToastParamsVariant] `json:"variant,required"` + Title param.Field[string] `json:"title"` +} + +func (r TuiShowToastParams) MarshalJSON() (data []byte, err error) { + return apijson.MarshalRoot(r) +} + +type TuiShowToastParamsVariant string + +const ( + TuiShowToastParamsVariantInfo TuiShowToastParamsVariant = "info" + TuiShowToastParamsVariantSuccess TuiShowToastParamsVariant = "success" + TuiShowToastParamsVariantWarning TuiShowToastParamsVariant = "warning" + TuiShowToastParamsVariantError TuiShowToastParamsVariant = "error" +) + +func (r TuiShowToastParamsVariant) IsKnown() bool { + switch r { + case TuiShowToastParamsVariantInfo, TuiShowToastParamsVariantSuccess, TuiShowToastParamsVariantWarning, TuiShowToastParamsVariantError: + return true + } + return false +} diff --git a/packages/sdk/go/tui_test.go b/packages/sdk/go/tui_test.go index f3260aaf..cb482226 100644 --- a/packages/sdk/go/tui_test.go +++ b/packages/sdk/go/tui_test.go @@ -171,6 +171,32 @@ func TestTuiOpenThemes(t *testing.T) { } } +func TestTuiShowToastWithOptionalParams(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.ShowToast(context.TODO(), opencode.TuiShowToastParams{ + Message: opencode.F("message"), + Variant: opencode.F(opencode.TuiShowToastParamsVariantInfo), + Title: opencode.F("title"), + }) + 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" diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index d2986238..1e0e1ae5 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -77,6 +77,8 @@ import type { TuiClearPromptResponses, TuiExecuteCommandData, TuiExecuteCommandResponses, + TuiShowToastData, + TuiShowToastResponses, AuthSetData, AuthSetResponses, AuthSetErrors, @@ -518,6 +520,20 @@ class Tui extends _HeyApiClient { }, }) } + + /** + * Show a toast notification in the TUI + */ + public showToast(options?: Options) { + return (options?.client ?? this._client).post({ + url: "/tui/show-toast", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }) + } } class Auth extends _HeyApiClient { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 54087908..81faf7bf 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1911,6 +1911,26 @@ export type TuiExecuteCommandResponses = { export type TuiExecuteCommandResponse = TuiExecuteCommandResponses[keyof TuiExecuteCommandResponses] +export type TuiShowToastData = { + body?: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + } + path?: never + query?: never + url: "/tui/show-toast" +} + +export type TuiShowToastResponses = { + /** + * Toast notification shown successfully + */ + 200: boolean +} + +export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] + export type AuthSetData = { body?: Auth path: { diff --git a/packages/sdk/stainless/stainless.yml b/packages/sdk/stainless/stainless.yml index db4afd8d..4ce3cd2a 100644 --- a/packages/sdk/stainless/stainless.yml +++ b/packages/sdk/stainless/stainless.yml @@ -146,6 +146,7 @@ resources: openThemes: post /tui/open-themes openModels: post /tui/open-models executeCommand: post /tui/execute-command + showToast: post /tui/show-toast settings: disable_mock_tests: true diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 7b5fc72c..97e5527b 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -705,6 +705,45 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { updated, cmd := a.executeCommand(commands.Command(command)) a = updated.(Model) cmds = append(cmds, cmd) + case "/tui/show-toast": + var body struct { + Title string `json:"title,omitempty"` + Message string `json:"message"` + Variant string `json:"variant"` + } + json.Unmarshal((msg.Body), &body) + + var toastCmd tea.Cmd + switch body.Variant { + case "info": + if body.Title != "" { + toastCmd = toast.NewInfoToast(body.Message, toast.WithTitle(body.Title)) + } else { + toastCmd = toast.NewInfoToast(body.Message) + } + case "success": + if body.Title != "" { + toastCmd = toast.NewSuccessToast(body.Message, toast.WithTitle(body.Title)) + } else { + toastCmd = toast.NewSuccessToast(body.Message) + } + case "warning": + if body.Title != "" { + toastCmd = toast.NewErrorToast(body.Message, toast.WithTitle(body.Title)) + } else { + toastCmd = toast.NewErrorToast(body.Message) + } + case "error": + if body.Title != "" { + toastCmd = toast.NewErrorToast(body.Message, toast.WithTitle(body.Title)) + } else { + toastCmd = toast.NewErrorToast(body.Message) + } + default: + slog.Error("Invalid toast variant", "variant", body.Variant) + return a, nil + } + cmds = append(cmds, toastCmd) default: break