summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-03 11:49:15 -0500
committeradamdottv <[email protected]>2025-07-03 11:49:15 -0500
commit5a0910ea79b3f219c64f922fc775636b2bfdf07c (patch)
tree6f3288101eb3aa99fdc31cfaf5e07e4990cc8954
parent1dffabcfdaeefd3bc08a51b625047185bade3a4d (diff)
downloadopencode-5a0910ea79b3f219c64f922fc775636b2bfdf07c.tar.gz
opencode-5a0910ea79b3f219c64f922fc775636b2bfdf07c.zip
chore: better local dev with stainless script
-rw-r--r--.gitignore1
-rw-r--r--package.json1
-rw-r--r--packages/tui/go.mod2
-rw-r--r--packages/tui/go.sum2
-rw-r--r--packages/tui/sdk/.devcontainer/devcontainer.json7
-rw-r--r--packages/tui/sdk/.github/workflows/ci.yml49
-rw-r--r--packages/tui/sdk/.gitignore4
-rw-r--r--packages/tui/sdk/.release-please-manifest.json3
-rw-r--r--packages/tui/sdk/.stats.yml4
-rw-r--r--packages/tui/sdk/Brewfile1
-rw-r--r--packages/tui/sdk/CHANGELOG.md73
-rw-r--r--packages/tui/sdk/CONTRIBUTING.md66
-rw-r--r--packages/tui/sdk/LICENSE201
-rw-r--r--packages/tui/sdk/README.md354
-rw-r--r--packages/tui/sdk/SECURITY.md27
-rw-r--r--packages/tui/sdk/aliases.go34
-rw-r--r--packages/tui/sdk/api.md110
-rw-r--r--packages/tui/sdk/app.go123
-rw-r--r--packages/tui/sdk/app_test.go58
-rw-r--r--packages/tui/sdk/client.go123
-rw-r--r--packages/tui/sdk/client_test.go332
-rw-r--r--packages/tui/sdk/config.go724
-rw-r--r--packages/tui/sdk/config_test.go58
-rw-r--r--packages/tui/sdk/event.go1180
-rw-r--r--packages/tui/sdk/examples/.keep4
-rw-r--r--packages/tui/sdk/field.go50
-rw-r--r--packages/tui/sdk/file.go143
-rw-r--r--packages/tui/sdk/file_test.go60
-rw-r--r--packages/tui/sdk/find.go213
-rw-r--r--packages/tui/sdk/find_test.go86
-rw-r--r--packages/tui/sdk/go.mod13
-rw-r--r--packages/tui/sdk/go.sum10
-rw-r--r--packages/tui/sdk/internal/apierror/apierror.go53
-rw-r--r--packages/tui/sdk/internal/apiform/encoder.go383
-rw-r--r--packages/tui/sdk/internal/apiform/form.go5
-rw-r--r--packages/tui/sdk/internal/apiform/form_test.go440
-rw-r--r--packages/tui/sdk/internal/apiform/tag.go48
-rw-r--r--packages/tui/sdk/internal/apijson/decoder.go670
-rw-r--r--packages/tui/sdk/internal/apijson/encoder.go398
-rw-r--r--packages/tui/sdk/internal/apijson/field.go41
-rw-r--r--packages/tui/sdk/internal/apijson/field_test.go66
-rw-r--r--packages/tui/sdk/internal/apijson/json_test.go617
-rw-r--r--packages/tui/sdk/internal/apijson/port.go120
-rw-r--r--packages/tui/sdk/internal/apijson/port_test.go257
-rw-r--r--packages/tui/sdk/internal/apijson/registry.go41
-rw-r--r--packages/tui/sdk/internal/apijson/tag.go47
-rw-r--r--packages/tui/sdk/internal/apiquery/encoder.go341
-rw-r--r--packages/tui/sdk/internal/apiquery/query.go50
-rw-r--r--packages/tui/sdk/internal/apiquery/query_test.go335
-rw-r--r--packages/tui/sdk/internal/apiquery/tag.go41
-rw-r--r--packages/tui/sdk/internal/param/field.go29
-rw-r--r--packages/tui/sdk/internal/requestconfig/requestconfig.go629
-rw-r--r--packages/tui/sdk/internal/testutil/testutil.go27
-rw-r--r--packages/tui/sdk/internal/version.go5
-rw-r--r--packages/tui/sdk/lib/.keep4
-rw-r--r--packages/tui/sdk/option/middleware.go38
-rw-r--r--packages/tui/sdk/option/requestoption.go266
-rw-r--r--packages/tui/sdk/packages/ssestream/ssestream.go181
-rw-r--r--packages/tui/sdk/release-please-config.json67
-rwxr-xr-xpackages/tui/sdk/scripts/bootstrap16
-rwxr-xr-xpackages/tui/sdk/scripts/format8
-rwxr-xr-xpackages/tui/sdk/scripts/lint8
-rwxr-xr-xpackages/tui/sdk/scripts/mock41
-rwxr-xr-xpackages/tui/sdk/scripts/test56
-rw-r--r--packages/tui/sdk/session.go1385
-rw-r--r--packages/tui/sdk/session_test.go259
-rw-r--r--packages/tui/sdk/shared/shared.go132
-rw-r--r--packages/tui/sdk/usage_test.go32
-rwxr-xr-xscripts/stainless26
-rw-r--r--stainless-workspace.json5
70 files changed, 11281 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
index a07a7493f..27316da64 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ node_modules
.env
.idea
.vscode
+openapi.json
diff --git a/package.json b/package.json
index ed4fcdeda..55242761b 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"packageManager": "[email protected]",
"scripts": {
"typecheck": "bun run --filter='*' typecheck",
+ "stainless": "bun run ./packages/opencode/src/index.ts serve ",
"postinstall": "./scripts/hooks"
},
"workspaces": {
diff --git a/packages/tui/go.mod b/packages/tui/go.mod
index 6cd1bae65..043d9fcd1 100644
--- a/packages/tui/go.mod
+++ b/packages/tui/go.mod
@@ -20,6 +20,8 @@ require (
rsc.io/qr v0.2.0
)
+replace github.com/sst/opencode-sdk-go => ./sdk
+
require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
require (
diff --git a/packages/tui/go.sum b/packages/tui/go.sum
index ac6981f28..295482734 100644
--- a/packages/tui/go.sum
+++ b/packages/tui/go.sum
@@ -181,8 +181,6 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/sst/opencode-sdk-go v0.1.0-alpha.8 h1:Tp7nbckbMCwAA/ieVZeeZCp79xXtrPMaWLRk5mhNwrw=
-github.com/sst/opencode-sdk-go v0.1.0-alpha.8/go.mod h1:uagorfAHZsVy6vf0xY6TlQraM4uCILdZ5tKKhl1oToM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
diff --git a/packages/tui/sdk/.devcontainer/devcontainer.json b/packages/tui/sdk/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..889ae3472
--- /dev/null
+++ b/packages/tui/sdk/.devcontainer/devcontainer.json
@@ -0,0 +1,7 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/debian
+{
+ "name": "Development",
+ "image": "mcr.microsoft.com/devcontainers/go:1.23-bookworm",
+ "postCreateCommand": "go mod tidy"
+}
diff --git a/packages/tui/sdk/.github/workflows/ci.yml b/packages/tui/sdk/.github/workflows/ci.yml
new file mode 100644
index 000000000..4bf1e907c
--- /dev/null
+++ b/packages/tui/sdk/.github/workflows/ci.yml
@@ -0,0 +1,49 @@
+name: CI
+on:
+ push:
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
+ pull_request:
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
+
+jobs:
+ lint:
+ timeout-minutes: 10
+ name: lint
+ runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: ./go.mod
+
+ - name: Run lints
+ run: ./scripts/lint
+ test:
+ timeout-minutes: 10
+ name: test
+ runs-on: ${{ github.repository == 'stainless-sdks/opencode-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: ./go.mod
+
+ - name: Bootstrap
+ run: ./scripts/bootstrap
+
+ - name: Run tests
+ run: ./scripts/test
diff --git a/packages/tui/sdk/.gitignore b/packages/tui/sdk/.gitignore
new file mode 100644
index 000000000..c6d050151
--- /dev/null
+++ b/packages/tui/sdk/.gitignore
@@ -0,0 +1,4 @@
+.prism.log
+codegen.log
+Brewfile.lock.json
+.idea/
diff --git a/packages/tui/sdk/.release-please-manifest.json b/packages/tui/sdk/.release-please-manifest.json
new file mode 100644
index 000000000..c373724dc
--- /dev/null
+++ b/packages/tui/sdk/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.1.0-alpha.8"
+} \ No newline at end of file
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
new file mode 100644
index 000000000..ab6c4a206
--- /dev/null
+++ b/packages/tui/sdk/.stats.yml
@@ -0,0 +1,4 @@
+configured_endpoints: 20
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-945f9da9e9a4c4008834deef63e4346c0076e020eed3d3c98c249095033c1ac5.yml
+openapi_spec_hash: 522a44f6cb0677435fe2ac7693848ad7
+config_hash: 6c8822d278ba83456e5eed6d774ca230
diff --git a/packages/tui/sdk/Brewfile b/packages/tui/sdk/Brewfile
new file mode 100644
index 000000000..577e34a4b
--- /dev/null
+++ b/packages/tui/sdk/Brewfile
@@ -0,0 +1 @@
+brew "go"
diff --git a/packages/tui/sdk/CHANGELOG.md b/packages/tui/sdk/CHANGELOG.md
new file mode 100644
index 000000000..bc407fad9
--- /dev/null
+++ b/packages/tui/sdk/CHANGELOG.md
@@ -0,0 +1,73 @@
+# Changelog
+
+## 0.1.0-alpha.8 (2025-07-02)
+
+Full Changelog: [v0.1.0-alpha.7...v0.1.0-alpha.8](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.7...v0.1.0-alpha.8)
+
+### Features
+
+* **api:** update via SDK Studio ([651e937](https://github.com/sst/opencode-sdk-go/commit/651e937c334e1caba3b968e6cac865c219879519))
+
+## 0.1.0-alpha.7 (2025-06-30)
+
+Full Changelog: [v0.1.0-alpha.6...v0.1.0-alpha.7](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.6...v0.1.0-alpha.7)
+
+### Features
+
+* **api:** update via SDK Studio ([13550a5](https://github.com/sst/opencode-sdk-go/commit/13550a5c65d77325e945ed99fe0799cd1107b775))
+* **api:** update via SDK Studio ([7b73730](https://github.com/sst/opencode-sdk-go/commit/7b73730c7fa62ba966dda3541c3e97b49be8d2bf))
+
+
+### Chores
+
+* **ci:** only run for pushes and fork pull requests ([bea59b8](https://github.com/sst/opencode-sdk-go/commit/bea59b886800ef555f89c47a9256d6392ed2e53d))
+
+## 0.1.0-alpha.6 (2025-06-28)
+
+Full Changelog: [v0.1.0-alpha.5...v0.1.0-alpha.6](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.5...v0.1.0-alpha.6)
+
+### Bug Fixes
+
+* don't try to deserialize as json when ResponseBodyInto is []byte ([5988d04](https://github.com/sst/opencode-sdk-go/commit/5988d04839cb78b6613057280b91b72a60fef33d))
+
+## 0.1.0-alpha.5 (2025-06-27)
+
+Full Changelog: [v0.1.0-alpha.4...v0.1.0-alpha.5](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.4...v0.1.0-alpha.5)
+
+### Features
+
+* **api:** update via SDK Studio ([9e39a59](https://github.com/sst/opencode-sdk-go/commit/9e39a59b3d5d1bd5e64633732521fb28362cc70e))
+
+## 0.1.0-alpha.4 (2025-06-27)
+
+Full Changelog: [v0.1.0-alpha.3...v0.1.0-alpha.4](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.3...v0.1.0-alpha.4)
+
+### Features
+
+* **api:** update via SDK Studio ([9609d1b](https://github.com/sst/opencode-sdk-go/commit/9609d1b1db7806d00cb846c9914cb4935cdedf52))
+
+## 0.1.0-alpha.3 (2025-06-27)
+
+Full Changelog: [v0.1.0-alpha.2...v0.1.0-alpha.3](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.2...v0.1.0-alpha.3)
+
+### Features
+
+* **api:** update via SDK Studio ([57f3230](https://github.com/sst/opencode-sdk-go/commit/57f32309023cc1f0f20c20d02a3907e390a71f61))
+
+## 0.1.0-alpha.2 (2025-06-27)
+
+Full Changelog: [v0.1.0-alpha.1...v0.1.0-alpha.2](https://github.com/sst/opencode-sdk-go/compare/v0.1.0-alpha.1...v0.1.0-alpha.2)
+
+### Features
+
+* **api:** update via SDK Studio ([a766f1c](https://github.com/sst/opencode-sdk-go/commit/a766f1c54f02bbc1380151b0e22d97cc2c5892e6))
+
+## 0.1.0-alpha.1 (2025-06-27)
+
+Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/sst/opencode-sdk-go/compare/v0.0.1-alpha.0...v0.1.0-alpha.1)
+
+### Features
+
+* **api:** update via SDK Studio ([27b7376](https://github.com/sst/opencode-sdk-go/commit/27b7376310466ee17a63f2104f546b53a2b8361a))
+* **api:** update via SDK Studio ([0a73e04](https://github.com/sst/opencode-sdk-go/commit/0a73e04c23c90b2061611edaa8fd6282dc0ce397))
+* **api:** update via SDK Studio ([9b7883a](https://github.com/sst/opencode-sdk-go/commit/9b7883a144eeac526d9d04538e0876a9d18bb844))
diff --git a/packages/tui/sdk/CONTRIBUTING.md b/packages/tui/sdk/CONTRIBUTING.md
new file mode 100644
index 000000000..34620a3c1
--- /dev/null
+++ b/packages/tui/sdk/CONTRIBUTING.md
@@ -0,0 +1,66 @@
+## Setting up the environment
+
+To set up the repository, run:
+
+```sh
+$ ./scripts/bootstrap
+$ ./scripts/build
+```
+
+This will install all the required dependencies and build the SDK.
+
+You can also [install go 1.18+ manually](https://go.dev/doc/install).
+
+## Modifying/Adding code
+
+Most of the SDK is generated code. Modifications to code will be persisted between generations, but may
+result in merge conflicts between manual patches and changes from the generator. The generator will never
+modify the contents of the `lib/` and `examples/` directories.
+
+## Adding and running examples
+
+All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.
+
+```go
+# add an example to examples/<your-example>/main.go
+
+package main
+
+func main() {
+ // ...
+}
+```
+
+```sh
+$ go run ./examples/<your-example>
+```
+
+## Using the repository from source
+
+To use a local version of this library from source in another project, edit the `go.mod` with a replace
+directive. This can be done through the CLI with the following:
+
+```sh
+$ go mod edit -replace github.com/sst/opencode-sdk-go=/path/to/opencode-sdk-go
+```
+
+## Running tests
+
+Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
+
+```sh
+# you will need npm installed
+$ npx prism mock path/to/your/openapi.yml
+```
+
+```sh
+$ ./scripts/test
+```
+
+## Formatting
+
+This library uses the standard gofmt code formatter:
+
+```sh
+$ ./scripts/format
+```
diff --git a/packages/tui/sdk/LICENSE b/packages/tui/sdk/LICENSE
new file mode 100644
index 000000000..a56ceacd7
--- /dev/null
+++ b/packages/tui/sdk/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2025 Opencode
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/tui/sdk/README.md b/packages/tui/sdk/README.md
new file mode 100644
index 000000000..2b5782347
--- /dev/null
+++ b/packages/tui/sdk/README.md
@@ -0,0 +1,354 @@
+# Opencode Go API Library
+
+<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go"><img src="https://pkg.go.dev/badge/github.com/sst/opencode-sdk-go.svg" alt="Go Reference"></a>
+
+The Opencode Go library provides convenient access to the [Opencode REST API](https://opencode.ai/docs)
+from applications written in Go.
+
+It is generated with [Stainless](https://www.stainless.com/).
+
+## Installation
+
+<!-- x-release-please-start-version -->
+
+```go
+import (
+ "github.com/sst/opencode-sdk-go" // imported as opencode
+)
+```
+
+<!-- x-release-please-end -->
+
+Or to pin the version:
+
+<!-- x-release-please-start-version -->
+
+```sh
+go get -u 'github.com/sst/[email protected]'
+```
+
+<!-- x-release-please-end -->
+
+## Requirements
+
+This library requires Go 1.18+.
+
+## Usage
+
+The full API of this library can be found in [api.md](api.md).
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/sst/opencode-sdk-go"
+)
+
+func main() {
+ client := opencode.NewClient()
+ events, err := client.Event.List(context.TODO())
+ if err != nil {
+ panic(err.Error())
+ }
+ fmt.Printf("%+v\n", events)
+}
+
+```
+
+### Request fields
+
+All request parameters are wrapped in a generic `Field` type,
+which we use to distinguish zero values from null or omitted fields.
+
+This prevents accidentally sending a zero value if you forget a required parameter,
+and enables explicitly sending `null`, `false`, `''`, or `0` on optional parameters.
+Any field not specified is not sent.
+
+To construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`.
+To send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example:
+
+```go
+params := FooParams{
+ Name: opencode.F("hello"),
+
+ // Explicitly send `"description": null`
+ Description: opencode.Null[string](),
+
+ Point: opencode.F(opencode.Point{
+ X: opencode.Int(0),
+ Y: opencode.Int(1),
+
+ // In cases where the API specifies a given type,
+ // but you want to send something else, use `Raw`:
+ Z: opencode.Raw[int64](0.01), // sends a float
+ }),
+}
+```
+
+### Response objects
+
+All fields in response structs are value types (not pointers or wrappers).
+
+If a given field is `null`, not present, or invalid, the corresponding field
+will simply be its zero value.
+
+All response structs also include a special `JSON` field, containing more detailed
+information about each property, which you can use like so:
+
+```go
+if res.Name == "" {
+ // true if `"name"` is either not present or explicitly null
+ res.JSON.Name.IsNull()
+
+ // true if the `"name"` key was not present in the response JSON at all
+ res.JSON.Name.IsMissing()
+
+ // When the API returns data that cannot be coerced to the expected type:
+ if res.JSON.Name.IsInvalid() {
+ raw := res.JSON.Name.Raw()
+
+ legacyName := struct{
+ First string `json:"first"`
+ Last string `json:"last"`
+ }{}
+ json.Unmarshal([]byte(raw), &legacyName)
+ name = legacyName.First + " " + legacyName.Last
+ }
+}
+```
+
+These `.JSON` structs also include an `Extras` map containing
+any properties in the json response that were not specified
+in the struct. This can be useful for API features not yet
+present in the SDK.
+
+```go
+body := res.JSON.ExtraFields["my_unexpected_field"].Raw()
+```
+
+### RequestOptions
+
+This library uses the functional options pattern. Functions defined in the
+`option` package return a `RequestOption`, which is a closure that mutates a
+`RequestConfig`. These options can be supplied to the client or at individual
+requests. For example:
+
+```go
+client := opencode.NewClient(
+ // Adds a header to every request made by the client
+ option.WithHeader("X-Some-Header", "custom_header_info"),
+)
+
+client.Event.List(context.TODO(), ...,
+ // Override the header
+ option.WithHeader("X-Some-Header", "some_other_custom_header_info"),
+ // Add an undocumented field to the request body, using sjson syntax
+ option.WithJSONSet("some.json.path", map[string]string{"my": "object"}),
+)
+```
+
+See the [full list of request options](https://pkg.go.dev/github.com/sst/opencode-sdk-go/option).
+
+### Pagination
+
+This library provides some conveniences for working with paginated list endpoints.
+
+You can use `.ListAutoPaging()` methods to iterate through items across all pages:
+
+Or you can use simple `.List()` methods to fetch a single page and receive a standard response object
+with additional helper methods like `.GetNextPage()`, e.g.:
+
+### Errors
+
+When the API returns a non-success status code, we return an error with type
+`*opencode.Error`. This contains the `StatusCode`, `*http.Request`, and
+`*http.Response` values of the request, as well as the JSON of the error body
+(much like other response objects in the SDK).
+
+To handle errors, we recommend that you use the `errors.As` pattern:
+
+```go
+_, err := client.Event.List(context.TODO())
+if err != nil {
+ var apierr *opencode.Error
+ if errors.As(err, &apierr) {
+ println(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request
+ println(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response
+ }
+ panic(err.Error()) // GET "/event": 400 Bad Request { ... }
+}
+```
+
+When other errors occur, they are returned unwrapped; for example,
+if HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.
+
+### Timeouts
+
+Requests do not time out by default; use context to configure a timeout for a request lifecycle.
+
+Note that if a request is [retried](#retries), the context timeout does not start over.
+To set a per-retry timeout, use `option.WithRequestTimeout()`.
+
+```go
+// This sets the timeout for the request, including all the retries.
+ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+defer cancel()
+client.Event.List(
+ ctx,
+ // This sets the per-retry timeout
+ option.WithRequestTimeout(20*time.Second),
+)
+```
+
+### File uploads
+
+Request parameters that correspond to file uploads in multipart requests are typed as
+`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form
+part with the file name of "anonymous_file" and content-type of "application/octet-stream".
+
+The file name and content-type can be customized by implementing `Name() string` or `ContentType()
+string` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a
+file returned by `os.Open` will be sent with the file name on disk.
+
+We also provide a helper `opencode.FileParam(reader io.Reader, filename string, contentType string)`
+which can be used to wrap any `io.Reader` with the appropriate file name and content type.
+
+### Retries
+
+Certain errors will be automatically retried 2 times by default, with a short exponential backoff.
+We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,
+and >=500 Internal errors.
+
+You can use the `WithMaxRetries` option to configure or disable this:
+
+```go
+// Configure the default for all requests:
+client := opencode.NewClient(
+ option.WithMaxRetries(0), // default is 2
+)
+
+// Override per-request:
+client.Event.List(context.TODO(), option.WithMaxRetries(5))
+```
+
+### Accessing raw response data (e.g. response headers)
+
+You can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when
+you need to examine response headers, status codes, or other details.
+
+```go
+// Create a variable to store the HTTP response
+var response *http.Response
+events, err := client.Event.List(context.TODO(), option.WithResponseInto(&response))
+if err != nil {
+ // handle error
+}
+fmt.Printf("%+v\n", events)
+
+fmt.Printf("Status Code: %d\n", response.StatusCode)
+fmt.Printf("Headers: %+#v\n", response.Header)
+```
+
+### Making custom/undocumented requests
+
+This library is typed for convenient access to the documented API. If you need to access undocumented
+endpoints, params, or response properties, the library can still be used.
+
+#### Undocumented endpoints
+
+To make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs.
+`RequestOptions` on the client, such as retries, will be respected when making these requests.
+
+```go
+var (
+ // params can be an io.Reader, a []byte, an encoding/json serializable object,
+ // or a "…Params" struct defined in this library.
+ params map[string]interface{}
+
+ // result can be an []byte, *http.Response, a encoding/json deserializable object,
+ // or a model defined in this library.
+ result *http.Response
+)
+err := client.Post(context.Background(), "/unspecified", params, &result)
+if err != nil {
+ …
+}
+```
+
+#### Undocumented request params
+
+To make requests using undocumented parameters, you may use either the `option.WithQuerySet()`
+or the `option.WithJSONSet()` methods.
+
+```go
+params := FooNewParams{
+ ID: opencode.F("id_xxxx"),
+ Data: opencode.F(FooNewParamsData{
+ FirstName: opencode.F("John"),
+ }),
+}
+client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe"))
+```
+
+#### Undocumented response properties
+
+To access undocumented response properties, you may either access the raw JSON of the response as a string
+with `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with
+`result.JSON.Foo.Raw()`.
+
+Any fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.
+
+### Middleware
+
+We provide `option.WithMiddleware` which applies the given
+middleware to requests.
+
+```go
+func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
+ // Before the request
+ start := time.Now()
+ LogReq(req)
+
+ // Forward the request to the next handler
+ res, err = next(req)
+
+ // Handle stuff after the request
+ end := time.Now()
+ LogRes(res, err, start - end)
+
+ return res, err
+}
+
+client := opencode.NewClient(
+ option.WithMiddleware(Logger),
+)
+```
+
+When multiple middlewares are provided as variadic arguments, the middlewares
+are applied left to right. If `option.WithMiddleware` is given
+multiple times, for example first in the client then the method, the
+middleware in the client will run first and the middleware given in the method
+will run next.
+
+You may also replace the default `http.Client` with
+`option.WithHTTPClient(client)`. Only one http client is
+accepted (this overwrites any previous client) and receives requests after any
+middleware has been applied.
+
+## Semantic versioning
+
+This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
+
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
+2. Changes that we do not expect to impact the vast majority of users in practice.
+
+We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
+
+We are keen for your feedback; please open an [issue](https://www.github.com/sst/opencode-sdk-go/issues) with questions, bugs, or suggestions.
+
+## Contributing
+
+See [the contributing documentation](./CONTRIBUTING.md).
diff --git a/packages/tui/sdk/SECURITY.md b/packages/tui/sdk/SECURITY.md
new file mode 100644
index 000000000..6912e12bc
--- /dev/null
+++ b/packages/tui/sdk/SECURITY.md
@@ -0,0 +1,27 @@
+# Security Policy
+
+## Reporting Security Issues
+
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+
+To report a security issue, please contact the Stainless team at [email protected].
+
+## Responsible Disclosure
+
+We appreciate the efforts of security researchers and individuals who help us maintain the security of
+SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
+disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
+before making any information public.
+
+## Reporting Non-SDK Related Security Issues
+
+If you encounter security issues that are not directly related to SDKs but pertain to the services
+or products provided by Opencode, please follow the respective company's security reporting guidelines.
+
+### Opencode Terms and Policies
+
+Please contact [email protected] for any questions or concerns regarding the security of our services.
+
+---
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
diff --git a/packages/tui/sdk/aliases.go b/packages/tui/sdk/aliases.go
new file mode 100644
index 000000000..84dd614a7
--- /dev/null
+++ b/packages/tui/sdk/aliases.go
@@ -0,0 +1,34 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "github.com/sst/opencode-sdk-go/internal/apierror"
+ "github.com/sst/opencode-sdk-go/shared"
+)
+
+type Error = apierror.Error
+
+// This is an alias to an internal type.
+type ProviderAuthError = shared.ProviderAuthError
+
+// This is an alias to an internal type.
+type ProviderAuthErrorData = shared.ProviderAuthErrorData
+
+// This is an alias to an internal type.
+type ProviderAuthErrorName = shared.ProviderAuthErrorName
+
+// This is an alias to an internal value.
+const ProviderAuthErrorNameProviderAuthError = shared.ProviderAuthErrorNameProviderAuthError
+
+// This is an alias to an internal type.
+type UnknownError = shared.UnknownError
+
+// This is an alias to an internal type.
+type UnknownErrorData = shared.UnknownErrorData
+
+// This is an alias to an internal type.
+type UnknownErrorName = shared.UnknownErrorName
+
+// This is an alias to an internal value.
+const UnknownErrorNameUnknownError = shared.UnknownErrorNameUnknownError
diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md
new file mode 100644
index 000000000..4ac9edcf6
--- /dev/null
+++ b/packages/tui/sdk/api.md
@@ -0,0 +1,110 @@
+# Shared Response Types
+
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#ProviderAuthError">ProviderAuthError</a>
+- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared">shared</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go/shared#UnknownError">UnknownError</a>
+
+# Event
+
+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#EventListResponse">EventListResponse</a>
+
+Methods:
+
+- <code title="get /event">client.Event.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#EventService.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#EventListResponse">EventListResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
+# App
+
+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#App">App</a>
+
+Methods:
+
+- <code title="get /app">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.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#App">App</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="post /app/init">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Init">Init</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/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
+# Find
+
+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#FindSymbolsResponse">FindSymbolsResponse</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#FindTextResponse">FindTextResponse</a>
+
+Methods:
+
+- <code title="get /find/file">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Files">Files</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <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#FindFilesParams">FindFilesParams</a>) ([]<a href="https://pkg.go.dev/builtin#string">string</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /find/symbol">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Symbols">Symbols</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <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#FindSymbolsParams">FindSymbolsParams</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#FindSymbolsResponse">FindSymbolsResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /find">client.Find.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FindService.Text">Text</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <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#FindTextParams">FindTextParams</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#FindTextResponse">FindTextResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
+# File
+
+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#FileReadResponse">FileReadResponse</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#FileStatusResponse">FileStatusResponse</a>
+
+Methods:
+
+- <code title="get /file">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Read">Read</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>, query <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#FileReadParams">FileReadParams</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#FileReadResponse">FileReadResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /file/status">client.File.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#FileService.Status">Status</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#FileStatusResponse">FileStatusResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
+# Config
+
+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#Config">Config</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#Keybinds">Keybinds</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#McpLocal">McpLocal</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#McpRemote">McpRemote</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#Model">Model</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#Provider">Provider</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#ConfigProvidersResponse">ConfigProvidersResponse</a>
+
+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>
+- <code title="get /config/providers">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Providers">Providers</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#ConfigProvidersResponse">ConfigProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+
+# Session
+
+Params 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#FilePartParam">FilePartParam</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#MessagePartUnionParam">MessagePartUnionParam</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#ReasoningPartParam">ReasoningPartParam</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#SourceURLPartParam">SourceURLPartParam</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#StepStartPartParam">StepStartPartParam</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#TextPartParam">TextPartParam</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#ToolCallParam">ToolCallParam</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#ToolInvocationPartParam">ToolInvocationPartParam</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#ToolPartialCallParam">ToolPartialCallParam</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#ToolResultParam">ToolResultParam</a>
+
+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#FilePart">FilePart</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#Message">Message</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#MessagePart">MessagePart</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#ReasoningPart">ReasoningPart</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/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SourceURLPart">SourceURLPart</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#StepStartPart">StepStartPart</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#TextPart">TextPart</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#ToolCall">ToolCall</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#ToolInvocationPart">ToolInvocationPart</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#ToolPartialCall">ToolPartialCall</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#ToolResult">ToolResult</a>
+
+Methods:
+
+- <code title="post /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.New">New</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#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="get /session">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.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#Session">Session</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="delete /session/{id}">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Delete">Delete</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}/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#Message">Message</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">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Messages">Messages</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#Message">Message</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="post /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Share">Share</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}/summarize">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Summarize">Summarize</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#SessionSummarizeParams">SessionSummarizeParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
+- <code title="delete /session/{id}/share">client.Session.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#SessionService.Unshare">Unshare</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>
diff --git a/packages/tui/sdk/app.go b/packages/tui/sdk/app.go
new file mode 100644
index 000000000..dc44a74b3
--- /dev/null
+++ b/packages/tui/sdk/app.go
@@ -0,0 +1,123 @@
+// 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"
+)
+
+// AppService 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 [NewAppService] method instead.
+type AppService struct {
+ Options []option.RequestOption
+}
+
+// NewAppService 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 NewAppService(opts ...option.RequestOption) (r *AppService) {
+ r = &AppService{}
+ r.Options = opts
+ return
+}
+
+// Get app info
+func (r *AppService) Get(ctx context.Context, opts ...option.RequestOption) (res *App, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "app"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// Initialize the app
+func (r *AppService) Init(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "app/init"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+type App struct {
+ Git bool `json:"git,required"`
+ Hostname string `json:"hostname,required"`
+ Path AppPath `json:"path,required"`
+ Time AppTime `json:"time,required"`
+ User string `json:"user,required"`
+ JSON appJSON `json:"-"`
+}
+
+// appJSON contains the JSON metadata for the struct [App]
+type appJSON struct {
+ Git apijson.Field
+ Hostname apijson.Field
+ Path apijson.Field
+ Time apijson.Field
+ User apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *App) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r appJSON) RawJSON() string {
+ return r.raw
+}
+
+type AppPath struct {
+ Config string `json:"config,required"`
+ Cwd string `json:"cwd,required"`
+ Data string `json:"data,required"`
+ Root string `json:"root,required"`
+ State string `json:"state,required"`
+ JSON appPathJSON `json:"-"`
+}
+
+// appPathJSON contains the JSON metadata for the struct [AppPath]
+type appPathJSON struct {
+ Config apijson.Field
+ Cwd apijson.Field
+ Data apijson.Field
+ Root apijson.Field
+ State apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *AppPath) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r appPathJSON) RawJSON() string {
+ return r.raw
+}
+
+type AppTime struct {
+ Initialized float64 `json:"initialized"`
+ JSON appTimeJSON `json:"-"`
+}
+
+// appTimeJSON contains the JSON metadata for the struct [AppTime]
+type appTimeJSON struct {
+ Initialized apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *AppTime) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r appTimeJSON) RawJSON() string {
+ return r.raw
+}
diff --git a/packages/tui/sdk/app_test.go b/packages/tui/sdk/app_test.go
new file mode 100644
index 000000000..f96495f1c
--- /dev/null
+++ b/packages/tui/sdk/app_test.go
@@ -0,0 +1,58 @@
+// 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 TestAppGet(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.App.Get(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 TestAppInit(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.App.Init(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/sdk/client.go b/packages/tui/sdk/client.go
new file mode 100644
index 000000000..955eb7d6b
--- /dev/null
+++ b/packages/tui/sdk/client.go
@@ -0,0 +1,123 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "net/http"
+ "os"
+
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+// Client creates a struct with services and top level methods that help with
+// interacting with the opencode API. You should not instantiate this client
+// directly, and instead use the [NewClient] method instead.
+type Client struct {
+ Options []option.RequestOption
+ Event *EventService
+ App *AppService
+ Find *FindService
+ File *FileService
+ Config *ConfigService
+ Session *SessionService
+}
+
+// DefaultClientOptions read from the environment (OPENCODE_BASE_URL). This should
+// be used to initialize new clients.
+func DefaultClientOptions() []option.RequestOption {
+ defaults := []option.RequestOption{option.WithEnvironmentProduction()}
+ if o, ok := os.LookupEnv("OPENCODE_BASE_URL"); ok {
+ defaults = append(defaults, option.WithBaseURL(o))
+ }
+ return defaults
+}
+
+// NewClient generates a new client with the default option read from the
+// environment (OPENCODE_BASE_URL). The option passed in as arguments are applied
+// after these default arguments, and all option will be passed down to the
+// services and requests that this client makes.
+func NewClient(opts ...option.RequestOption) (r *Client) {
+ opts = append(DefaultClientOptions(), opts...)
+
+ r = &Client{Options: opts}
+
+ r.Event = NewEventService(opts...)
+ r.App = NewAppService(opts...)
+ r.Find = NewFindService(opts...)
+ r.File = NewFileService(opts...)
+ r.Config = NewConfigService(opts...)
+ r.Session = NewSessionService(opts...)
+
+ return
+}
+
+// Execute makes a request with the given context, method, URL, request params,
+// response, and request options. This is useful for hitting undocumented endpoints
+// while retaining the base URL, auth, retries, and other options from the client.
+//
+// If a byte slice or an [io.Reader] is supplied to params, it will be used as-is
+// for the request body.
+//
+// The params is by default serialized into the body using [encoding/json]. If your
+// type implements a MarshalJSON function, it will be used instead to serialize the
+// request. If a URLQuery method is implemented, the returned [url.Values] will be
+// used as query strings to the url.
+//
+// If your params struct uses [param.Field], you must provide either [MarshalJSON],
+// [URLQuery], and/or [MarshalForm] functions. It is undefined behavior to use a
+// struct uses [param.Field] without specifying how it is serialized.
+//
+// Any "…Params" object defined in this library can be used as the request
+// argument. Note that 'path' arguments will not be forwarded into the url.
+//
+// The response body will be deserialized into the res variable, depending on its
+// type:
+//
+// - A pointer to a [*http.Response] is populated by the raw response.
+// - A pointer to a byte array will be populated with the contents of the request
+// body.
+// - A pointer to any other type uses this library's default JSON decoding, which
+// respects UnmarshalJSON if it is defined on the type.
+// - A nil value will not read the response body.
+//
+// For even greater flexibility, see [option.WithResponseInto] and
+// [option.WithResponseBodyInto].
+func (r *Client) Execute(ctx context.Context, method string, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
+ opts = append(r.Options, opts...)
+ return requestconfig.ExecuteNewRequest(ctx, method, path, params, res, opts...)
+}
+
+// Get makes a GET request with the given URL, params, and optionally deserializes
+// to a response. See [Execute] documentation on the params and response.
+func (r *Client) Get(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
+ return r.Execute(ctx, http.MethodGet, path, params, res, opts...)
+}
+
+// Post makes a POST request with the given URL, params, and optionally
+// deserializes to a response. See [Execute] documentation on the params and
+// response.
+func (r *Client) Post(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
+ return r.Execute(ctx, http.MethodPost, path, params, res, opts...)
+}
+
+// Put makes a PUT request with the given URL, params, and optionally deserializes
+// to a response. See [Execute] documentation on the params and response.
+func (r *Client) Put(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
+ return r.Execute(ctx, http.MethodPut, path, params, res, opts...)
+}
+
+// Patch makes a PATCH request with the given URL, params, and optionally
+// deserializes to a response. See [Execute] documentation on the params and
+// response.
+func (r *Client) Patch(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
+ return r.Execute(ctx, http.MethodPatch, path, params, res, opts...)
+}
+
+// Delete makes a DELETE request with the given URL, params, and optionally
+// deserializes to a response. See [Execute] documentation on the params and
+// response.
+func (r *Client) Delete(ctx context.Context, path string, params interface{}, res interface{}, opts ...option.RequestOption) error {
+ return r.Execute(ctx, http.MethodDelete, path, params, res, opts...)
+}
diff --git a/packages/tui/sdk/client_test.go b/packages/tui/sdk/client_test.go
new file mode 100644
index 000000000..e75d64925
--- /dev/null
+++ b/packages/tui/sdk/client_test.go
@@ -0,0 +1,332 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode_test
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/http"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/sst/opencode-sdk-go"
+ "github.com/sst/opencode-sdk-go/internal"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+type closureTransport struct {
+ fn func(req *http.Request) (*http.Response, error)
+}
+
+func (t *closureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ return t.fn(req)
+}
+
+func TestUserAgentHeader(t *testing.T) {
+ var userAgent string
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ userAgent = req.Header.Get("User-Agent")
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ }, nil
+ },
+ },
+ }),
+ )
+ client.Event.List(context.Background())
+ if userAgent != fmt.Sprintf("Opencode/Go %s", internal.PackageVersion) {
+ t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent)
+ }
+}
+
+func TestRetryAfter(t *testing.T) {
+ retryCountHeaders := make([]string, 0)
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
+ return &http.Response{
+ StatusCode: http.StatusTooManyRequests,
+ Header: http.Header{
+ http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
+ },
+ }, nil
+ },
+ },
+ }),
+ )
+ _, err := client.Event.List(context.Background())
+ if err == nil {
+ t.Error("Expected there to be a cancel error")
+ }
+
+ attempts := len(retryCountHeaders)
+ if attempts != 3 {
+ t.Errorf("Expected %d attempts, got %d", 3, attempts)
+ }
+
+ expectedRetryCountHeaders := []string{"0", "1", "2"}
+ if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
+ t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
+ }
+}
+
+func TestDeleteRetryCountHeader(t *testing.T) {
+ retryCountHeaders := make([]string, 0)
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
+ return &http.Response{
+ StatusCode: http.StatusTooManyRequests,
+ Header: http.Header{
+ http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
+ },
+ }, nil
+ },
+ },
+ }),
+ option.WithHeaderDel("X-Stainless-Retry-Count"),
+ )
+ _, err := client.Event.List(context.Background())
+ if err == nil {
+ t.Error("Expected there to be a cancel error")
+ }
+
+ expectedRetryCountHeaders := []string{"", "", ""}
+ if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
+ t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
+ }
+}
+
+func TestOverwriteRetryCountHeader(t *testing.T) {
+ retryCountHeaders := make([]string, 0)
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ retryCountHeaders = append(retryCountHeaders, req.Header.Get("X-Stainless-Retry-Count"))
+ return &http.Response{
+ StatusCode: http.StatusTooManyRequests,
+ Header: http.Header{
+ http.CanonicalHeaderKey("Retry-After"): []string{"0.1"},
+ },
+ }, nil
+ },
+ },
+ }),
+ option.WithHeader("X-Stainless-Retry-Count", "42"),
+ )
+ _, err := client.Event.List(context.Background())
+ if err == nil {
+ t.Error("Expected there to be a cancel error")
+ }
+
+ expectedRetryCountHeaders := []string{"42", "42", "42"}
+ if !reflect.DeepEqual(retryCountHeaders, expectedRetryCountHeaders) {
+ t.Errorf("Expected %v retry count headers, got %v", expectedRetryCountHeaders, retryCountHeaders)
+ }
+}
+
+func TestRetryAfterMs(t *testing.T) {
+ attempts := 0
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ attempts++
+ return &http.Response{
+ StatusCode: http.StatusTooManyRequests,
+ Header: http.Header{
+ http.CanonicalHeaderKey("Retry-After-Ms"): []string{"100"},
+ },
+ }, nil
+ },
+ },
+ }),
+ )
+ _, err := client.Event.List(context.Background())
+ if err == nil {
+ t.Error("Expected there to be a cancel error")
+ }
+ if want := 3; attempts != want {
+ t.Errorf("Expected %d attempts, got %d", want, attempts)
+ }
+}
+
+func TestContextCancel(t *testing.T) {
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ <-req.Context().Done()
+ return nil, req.Context().Err()
+ },
+ },
+ }),
+ )
+ cancelCtx, cancel := context.WithCancel(context.Background())
+ cancel()
+ _, err := client.Event.List(cancelCtx)
+ if err == nil {
+ t.Error("Expected there to be a cancel error")
+ }
+}
+
+func TestContextCancelDelay(t *testing.T) {
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ <-req.Context().Done()
+ return nil, req.Context().Err()
+ },
+ },
+ }),
+ )
+ cancelCtx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond)
+ defer cancel()
+ _, err := client.Event.List(cancelCtx)
+ if err == nil {
+ t.Error("expected there to be a cancel error")
+ }
+}
+
+func TestContextDeadline(t *testing.T) {
+ testTimeout := time.After(3 * time.Second)
+ testDone := make(chan struct{})
+
+ deadline := time.Now().Add(100 * time.Millisecond)
+ deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
+ defer cancel()
+
+ go func() {
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ <-req.Context().Done()
+ return nil, req.Context().Err()
+ },
+ },
+ }),
+ )
+ _, err := client.Event.List(deadlineCtx)
+ if err == nil {
+ t.Error("expected there to be a deadline error")
+ }
+ close(testDone)
+ }()
+
+ select {
+ case <-testTimeout:
+ t.Fatal("client didn't finish in time")
+ case <-testDone:
+ if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
+ t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
+ }
+ }
+}
+
+func TestContextDeadlineStreaming(t *testing.T) {
+ testTimeout := time.After(3 * time.Second)
+ testDone := make(chan struct{})
+
+ deadline := time.Now().Add(100 * time.Millisecond)
+ deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)
+ defer cancel()
+
+ go func() {
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 200,
+ Status: "200 OK",
+ Body: io.NopCloser(
+ io.Reader(readerFunc(func([]byte) (int, error) {
+ <-req.Context().Done()
+ return 0, req.Context().Err()
+ })),
+ ),
+ }, nil
+ },
+ },
+ }),
+ )
+ stream := client.Event.ListStreaming(deadlineCtx)
+ for stream.Next() {
+ _ = stream.Current()
+ }
+ if stream.Err() == nil {
+ t.Error("expected there to be a deadline error")
+ }
+ close(testDone)
+ }()
+
+ select {
+ case <-testTimeout:
+ t.Fatal("client didn't finish in time")
+ case <-testDone:
+ if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
+ t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
+ }
+ }
+}
+
+func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) {
+ testTimeout := time.After(3 * time.Second)
+ testDone := make(chan struct{})
+ deadline := time.Now().Add(100 * time.Millisecond)
+
+ go func() {
+ client := opencode.NewClient(
+ option.WithHTTPClient(&http.Client{
+ Transport: &closureTransport{
+ fn: func(req *http.Request) (*http.Response, error) {
+ return &http.Response{
+ StatusCode: 200,
+ Status: "200 OK",
+ Body: io.NopCloser(
+ io.Reader(readerFunc(func([]byte) (int, error) {
+ <-req.Context().Done()
+ return 0, req.Context().Err()
+ })),
+ ),
+ }, nil
+ },
+ },
+ }),
+ )
+ stream := client.Event.ListStreaming(context.Background(), option.WithRequestTimeout((100 * time.Millisecond)))
+ for stream.Next() {
+ _ = stream.Current()
+ }
+ if stream.Err() == nil {
+ t.Error("expected there to be a deadline error")
+ }
+ close(testDone)
+ }()
+
+ select {
+ case <-testTimeout:
+ t.Fatal("client didn't finish in time")
+ case <-testDone:
+ if diff := time.Since(deadline); diff < -30*time.Millisecond || 30*time.Millisecond < diff {
+ t.Fatalf("client did not return within 30ms of context deadline, got %s", diff)
+ }
+ }
+}
+
+type readerFunc func([]byte) (int, error)
+
+func (f readerFunc) Read(p []byte) (int, error) { return f(p) }
+func (f readerFunc) Close() error { return nil }
diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go
new file mode 100644
index 000000000..39da2f942
--- /dev/null
+++ b/packages/tui/sdk/config.go
@@ -0,0 +1,724 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "net/http"
+ "reflect"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+ "github.com/tidwall/gjson"
+)
+
+// ConfigService 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 [NewConfigService] method instead.
+type ConfigService struct {
+ Options []option.RequestOption
+}
+
+// NewConfigService 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 NewConfigService(opts ...option.RequestOption) (r *ConfigService) {
+ r = &ConfigService{}
+ r.Options = opts
+ return
+}
+
+// Get config info
+func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (res *Config, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "config"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// List all providers
+func (r *ConfigService) Providers(ctx context.Context, opts ...option.RequestOption) (res *ConfigProvidersResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "config/providers"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+type Config struct {
+ // JSON schema reference for configuration validation
+ Schema string `json:"$schema"`
+ // Share newly created sessions automatically
+ Autoshare bool `json:"autoshare"`
+ // Automatically update to the latest version
+ Autoupdate bool `json:"autoupdate"`
+ // Disable providers that are loaded automatically
+ DisabledProviders []string `json:"disabled_providers"`
+ Experimental ConfigExperimental `json:"experimental"`
+ // Additional instruction files or patterns to include
+ Instructions []string `json:"instructions"`
+ // Custom keybind configurations
+ Keybinds Keybinds `json:"keybinds"`
+ // MCP (Model Context Protocol) server configurations
+ Mcp map[string]ConfigMcp `json:"mcp"`
+ // Model to use in the format of provider/model, eg anthropic/claude-2
+ Model string `json:"model"`
+ // Custom provider configurations and model overrides
+ Provider map[string]ConfigProvider `json:"provider"`
+ // Theme name to use for the interface
+ Theme string `json:"theme"`
+ JSON configJSON `json:"-"`
+}
+
+// configJSON contains the JSON metadata for the struct [Config]
+type configJSON struct {
+ Schema apijson.Field
+ Autoshare apijson.Field
+ Autoupdate apijson.Field
+ DisabledProviders apijson.Field
+ Experimental apijson.Field
+ Instructions apijson.Field
+ Keybinds apijson.Field
+ Mcp apijson.Field
+ Model apijson.Field
+ Provider apijson.Field
+ Theme apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Config) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigExperimental struct {
+ Hook ConfigExperimentalHook `json:"hook"`
+ JSON configExperimentalJSON `json:"-"`
+}
+
+// configExperimentalJSON contains the JSON metadata for the struct
+// [ConfigExperimental]
+type configExperimentalJSON struct {
+ Hook apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigExperimental) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configExperimentalJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigExperimentalHook struct {
+ FileEdited map[string][]ConfigExperimentalHookFileEdited `json:"file_edited"`
+ SessionCompleted []ConfigExperimentalHookSessionCompleted `json:"session_completed"`
+ JSON configExperimentalHookJSON `json:"-"`
+}
+
+// configExperimentalHookJSON contains the JSON metadata for the struct
+// [ConfigExperimentalHook]
+type configExperimentalHookJSON struct {
+ FileEdited apijson.Field
+ SessionCompleted apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigExperimentalHook) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configExperimentalHookJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigExperimentalHookFileEdited struct {
+ Command []string `json:"command,required"`
+ Environment map[string]string `json:"environment"`
+ JSON configExperimentalHookFileEditedJSON `json:"-"`
+}
+
+// configExperimentalHookFileEditedJSON contains the JSON metadata for the struct
+// [ConfigExperimentalHookFileEdited]
+type configExperimentalHookFileEditedJSON struct {
+ Command apijson.Field
+ Environment apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigExperimentalHookFileEdited) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configExperimentalHookFileEditedJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigExperimentalHookSessionCompleted struct {
+ Command []string `json:"command,required"`
+ Environment map[string]string `json:"environment"`
+ JSON configExperimentalHookSessionCompletedJSON `json:"-"`
+}
+
+// configExperimentalHookSessionCompletedJSON contains the JSON metadata for the
+// struct [ConfigExperimentalHookSessionCompleted]
+type configExperimentalHookSessionCompletedJSON struct {
+ Command apijson.Field
+ Environment apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigExperimentalHookSessionCompleted) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configExperimentalHookSessionCompletedJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigMcp struct {
+ // Type of MCP server connection
+ Type ConfigMcpType `json:"type,required"`
+ // This field can have the runtime type of [[]string].
+ Command interface{} `json:"command"`
+ // Enable or disable the MCP server on startup
+ Enabled bool `json:"enabled"`
+ // This field can have the runtime type of [map[string]string].
+ Environment interface{} `json:"environment"`
+ // URL of the remote MCP server
+ URL string `json:"url"`
+ JSON configMcpJSON `json:"-"`
+ union ConfigMcpUnion
+}
+
+// configMcpJSON contains the JSON metadata for the struct [ConfigMcp]
+type configMcpJSON struct {
+ Type apijson.Field
+ Command apijson.Field
+ Enabled apijson.Field
+ Environment apijson.Field
+ URL apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r configMcpJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *ConfigMcp) UnmarshalJSON(data []byte) (err error) {
+ *r = ConfigMcp{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [ConfigMcpUnion] interface which you can cast to the specific
+// types for more type safety.
+//
+// Possible runtime types of the union are [McpLocal], [McpRemote].
+func (r ConfigMcp) AsUnion() ConfigMcpUnion {
+ return r.union
+}
+
+// Union satisfied by [McpLocal] or [McpRemote].
+type ConfigMcpUnion interface {
+ implementsConfigMcp()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*ConfigMcpUnion)(nil)).Elem(),
+ "type",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(McpLocal{}),
+ DiscriminatorValue: "local",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(McpRemote{}),
+ DiscriminatorValue: "remote",
+ },
+ )
+}
+
+// Type of MCP server connection
+type ConfigMcpType string
+
+const (
+ ConfigMcpTypeLocal ConfigMcpType = "local"
+ ConfigMcpTypeRemote ConfigMcpType = "remote"
+)
+
+func (r ConfigMcpType) IsKnown() bool {
+ switch r {
+ case ConfigMcpTypeLocal, ConfigMcpTypeRemote:
+ return true
+ }
+ return false
+}
+
+type ConfigProvider struct {
+ Models map[string]ConfigProviderModel `json:"models,required"`
+ ID string `json:"id"`
+ API string `json:"api"`
+ Env []string `json:"env"`
+ Name string `json:"name"`
+ Npm string `json:"npm"`
+ Options map[string]interface{} `json:"options"`
+ JSON configProviderJSON `json:"-"`
+}
+
+// configProviderJSON contains the JSON metadata for the struct [ConfigProvider]
+type configProviderJSON struct {
+ Models apijson.Field
+ ID apijson.Field
+ API apijson.Field
+ Env apijson.Field
+ Name apijson.Field
+ Npm apijson.Field
+ Options apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigProvider) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configProviderJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigProviderModel struct {
+ ID string `json:"id"`
+ Attachment bool `json:"attachment"`
+ Cost ConfigProviderModelsCost `json:"cost"`
+ Limit ConfigProviderModelsLimit `json:"limit"`
+ Name string `json:"name"`
+ Options map[string]interface{} `json:"options"`
+ Reasoning bool `json:"reasoning"`
+ ReleaseDate string `json:"release_date"`
+ Temperature bool `json:"temperature"`
+ ToolCall bool `json:"tool_call"`
+ JSON configProviderModelJSON `json:"-"`
+}
+
+// configProviderModelJSON contains the JSON metadata for the struct
+// [ConfigProviderModel]
+type configProviderModelJSON struct {
+ ID apijson.Field
+ Attachment apijson.Field
+ Cost apijson.Field
+ Limit apijson.Field
+ Name apijson.Field
+ Options apijson.Field
+ Reasoning apijson.Field
+ ReleaseDate apijson.Field
+ Temperature apijson.Field
+ ToolCall apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigProviderModel) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configProviderModelJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigProviderModelsCost struct {
+ Input float64 `json:"input,required"`
+ Output float64 `json:"output,required"`
+ CacheRead float64 `json:"cache_read"`
+ CacheWrite float64 `json:"cache_write"`
+ JSON configProviderModelsCostJSON `json:"-"`
+}
+
+// configProviderModelsCostJSON contains the JSON metadata for the struct
+// [ConfigProviderModelsCost]
+type configProviderModelsCostJSON struct {
+ Input apijson.Field
+ Output apijson.Field
+ CacheRead apijson.Field
+ CacheWrite apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigProviderModelsCost) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configProviderModelsCostJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigProviderModelsLimit struct {
+ Context float64 `json:"context,required"`
+ Output float64 `json:"output,required"`
+ JSON configProviderModelsLimitJSON `json:"-"`
+}
+
+// configProviderModelsLimitJSON contains the JSON metadata for the struct
+// [ConfigProviderModelsLimit]
+type configProviderModelsLimitJSON struct {
+ Context apijson.Field
+ Output apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigProviderModelsLimit) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configProviderModelsLimitJSON) RawJSON() string {
+ return r.raw
+}
+
+type Keybinds struct {
+ // Exit the application
+ AppExit string `json:"app_exit"`
+ // Open external editor
+ EditorOpen string `json:"editor_open"`
+ // Show help dialog
+ Help string `json:"help"`
+ // Navigate to next history item
+ HistoryNext string `json:"history_next"`
+ // Navigate to previous history item
+ HistoryPrevious string `json:"history_previous"`
+ // Clear input field
+ InputClear string `json:"input_clear"`
+ // Insert newline in input
+ InputNewline string `json:"input_newline"`
+ // Paste from clipboard
+ InputPaste string `json:"input_paste"`
+ // Submit input
+ InputSubmit string `json:"input_submit"`
+ // Leader key for keybind combinations
+ Leader string `json:"leader"`
+ // Navigate to first message
+ MessagesFirst string `json:"messages_first"`
+ // Scroll messages down by half page
+ MessagesHalfPageDown string `json:"messages_half_page_down"`
+ // Scroll messages up by half page
+ MessagesHalfPageUp string `json:"messages_half_page_up"`
+ // Navigate to last message
+ MessagesLast string `json:"messages_last"`
+ // Navigate to next message
+ MessagesNext string `json:"messages_next"`
+ // Scroll messages down by one page
+ MessagesPageDown string `json:"messages_page_down"`
+ // Scroll messages up by one page
+ MessagesPageUp string `json:"messages_page_up"`
+ // Navigate to previous message
+ MessagesPrevious string `json:"messages_previous"`
+ // List available models
+ ModelList string `json:"model_list"`
+ // Initialize project configuration
+ ProjectInit string `json:"project_init"`
+ // Toggle compact mode for session
+ SessionCompact string `json:"session_compact"`
+ // Interrupt current session
+ SessionInterrupt string `json:"session_interrupt"`
+ // List all sessions
+ SessionList string `json:"session_list"`
+ // Create a new session
+ SessionNew string `json:"session_new"`
+ // Share current session
+ SessionShare string `json:"session_share"`
+ // List available themes
+ ThemeList string `json:"theme_list"`
+ // Show tool details
+ ToolDetails string `json:"tool_details"`
+ JSON keybindsJSON `json:"-"`
+}
+
+// keybindsJSON contains the JSON metadata for the struct [Keybinds]
+type keybindsJSON struct {
+ AppExit apijson.Field
+ EditorOpen apijson.Field
+ Help apijson.Field
+ HistoryNext apijson.Field
+ HistoryPrevious apijson.Field
+ InputClear apijson.Field
+ InputNewline apijson.Field
+ InputPaste apijson.Field
+ InputSubmit apijson.Field
+ Leader apijson.Field
+ MessagesFirst apijson.Field
+ MessagesHalfPageDown apijson.Field
+ MessagesHalfPageUp apijson.Field
+ MessagesLast apijson.Field
+ MessagesNext apijson.Field
+ MessagesPageDown apijson.Field
+ MessagesPageUp apijson.Field
+ MessagesPrevious apijson.Field
+ ModelList apijson.Field
+ ProjectInit apijson.Field
+ SessionCompact apijson.Field
+ SessionInterrupt apijson.Field
+ SessionList apijson.Field
+ SessionNew apijson.Field
+ SessionShare apijson.Field
+ ThemeList apijson.Field
+ ToolDetails apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Keybinds) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r keybindsJSON) RawJSON() string {
+ return r.raw
+}
+
+type McpLocal struct {
+ // Command and arguments to run the MCP server
+ Command []string `json:"command,required"`
+ // Type of MCP server connection
+ Type McpLocalType `json:"type,required"`
+ // Enable or disable the MCP server on startup
+ Enabled bool `json:"enabled"`
+ // Environment variables to set when running the MCP server
+ Environment map[string]string `json:"environment"`
+ JSON mcpLocalJSON `json:"-"`
+}
+
+// mcpLocalJSON contains the JSON metadata for the struct [McpLocal]
+type mcpLocalJSON struct {
+ Command apijson.Field
+ Type apijson.Field
+ Enabled apijson.Field
+ Environment apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *McpLocal) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r mcpLocalJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r McpLocal) implementsConfigMcp() {}
+
+// Type of MCP server connection
+type McpLocalType string
+
+const (
+ McpLocalTypeLocal McpLocalType = "local"
+)
+
+func (r McpLocalType) IsKnown() bool {
+ switch r {
+ case McpLocalTypeLocal:
+ return true
+ }
+ return false
+}
+
+type McpRemote struct {
+ // Type of MCP server connection
+ Type McpRemoteType `json:"type,required"`
+ // URL of the remote MCP server
+ URL string `json:"url,required"`
+ // Enable or disable the MCP server on startup
+ Enabled bool `json:"enabled"`
+ JSON mcpRemoteJSON `json:"-"`
+}
+
+// mcpRemoteJSON contains the JSON metadata for the struct [McpRemote]
+type mcpRemoteJSON struct {
+ Type apijson.Field
+ URL apijson.Field
+ Enabled apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *McpRemote) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r mcpRemoteJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r McpRemote) implementsConfigMcp() {}
+
+// Type of MCP server connection
+type McpRemoteType string
+
+const (
+ McpRemoteTypeRemote McpRemoteType = "remote"
+)
+
+func (r McpRemoteType) IsKnown() bool {
+ switch r {
+ case McpRemoteTypeRemote:
+ return true
+ }
+ return false
+}
+
+type Model struct {
+ ID string `json:"id,required"`
+ Attachment bool `json:"attachment,required"`
+ Cost ModelCost `json:"cost,required"`
+ Limit ModelLimit `json:"limit,required"`
+ Name string `json:"name,required"`
+ Options map[string]interface{} `json:"options,required"`
+ Reasoning bool `json:"reasoning,required"`
+ ReleaseDate string `json:"release_date,required"`
+ Temperature bool `json:"temperature,required"`
+ ToolCall bool `json:"tool_call,required"`
+ JSON modelJSON `json:"-"`
+}
+
+// modelJSON contains the JSON metadata for the struct [Model]
+type modelJSON struct {
+ ID apijson.Field
+ Attachment apijson.Field
+ Cost apijson.Field
+ Limit apijson.Field
+ Name apijson.Field
+ Options apijson.Field
+ Reasoning apijson.Field
+ ReleaseDate apijson.Field
+ Temperature apijson.Field
+ ToolCall apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Model) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelJSON) RawJSON() string {
+ return r.raw
+}
+
+type ModelCost struct {
+ Input float64 `json:"input,required"`
+ Output float64 `json:"output,required"`
+ CacheRead float64 `json:"cache_read"`
+ CacheWrite float64 `json:"cache_write"`
+ JSON modelCostJSON `json:"-"`
+}
+
+// modelCostJSON contains the JSON metadata for the struct [ModelCost]
+type modelCostJSON struct {
+ Input apijson.Field
+ Output apijson.Field
+ CacheRead apijson.Field
+ CacheWrite apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ModelCost) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelCostJSON) RawJSON() string {
+ return r.raw
+}
+
+type ModelLimit struct {
+ Context float64 `json:"context,required"`
+ Output float64 `json:"output,required"`
+ JSON modelLimitJSON `json:"-"`
+}
+
+// modelLimitJSON contains the JSON metadata for the struct [ModelLimit]
+type modelLimitJSON struct {
+ Context apijson.Field
+ Output apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ModelLimit) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelLimitJSON) RawJSON() string {
+ return r.raw
+}
+
+type Provider struct {
+ ID string `json:"id,required"`
+ Env []string `json:"env,required"`
+ Models map[string]Model `json:"models,required"`
+ Name string `json:"name,required"`
+ API string `json:"api"`
+ Npm string `json:"npm"`
+ JSON providerJSON `json:"-"`
+}
+
+// providerJSON contains the JSON metadata for the struct [Provider]
+type providerJSON struct {
+ ID apijson.Field
+ Env apijson.Field
+ Models apijson.Field
+ Name apijson.Field
+ API apijson.Field
+ Npm apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Provider) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r providerJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigProvidersResponse struct {
+ Default map[string]string `json:"default,required"`
+ Providers []Provider `json:"providers,required"`
+ JSON configProvidersResponseJSON `json:"-"`
+}
+
+// configProvidersResponseJSON contains the JSON metadata for the struct
+// [ConfigProvidersResponse]
+type configProvidersResponseJSON struct {
+ Default apijson.Field
+ Providers apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigProvidersResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configProvidersResponseJSON) RawJSON() string {
+ return r.raw
+}
diff --git a/packages/tui/sdk/config_test.go b/packages/tui/sdk/config_test.go
new file mode 100644
index 000000000..57a1d158d
--- /dev/null
+++ b/packages/tui/sdk/config_test.go
@@ -0,0 +1,58 @@
+// 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 TestConfigGet(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.Config.Get(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 TestConfigProviders(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.Config.Providers(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/sdk/event.go b/packages/tui/sdk/event.go
new file mode 100644
index 000000000..ed92b2ae3
--- /dev/null
+++ b/packages/tui/sdk/event.go
@@ -0,0 +1,1180 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "net/http"
+ "reflect"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+ "github.com/sst/opencode-sdk-go/packages/ssestream"
+ "github.com/sst/opencode-sdk-go/shared"
+ "github.com/tidwall/gjson"
+)
+
+// EventService 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 [NewEventService] method instead.
+type EventService struct {
+ Options []option.RequestOption
+}
+
+// NewEventService 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 NewEventService(opts ...option.RequestOption) (r *EventService) {
+ r = &EventService{}
+ r.Options = opts
+ return
+}
+
+// Get events
+func (r *EventService) ListStreaming(ctx context.Context, opts ...option.RequestOption) (stream *ssestream.Stream[EventListResponse]) {
+ var (
+ raw *http.Response
+ err error
+ )
+ opts = append(r.Options[:], opts...)
+ path := "event"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...)
+ return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err)
+}
+
+type EventListResponse struct {
+ // This field can have the runtime type of
+ // [EventListResponseEventLspClientDiagnosticsProperties],
+ // [EventListResponseEventPermissionUpdatedProperties],
+ // [EventListResponseEventFileEditedProperties],
+ // [EventListResponseEventStorageWriteProperties],
+ // [EventListResponseEventInstallationUpdatedProperties],
+ // [EventListResponseEventMessageUpdatedProperties],
+ // [EventListResponseEventMessageRemovedProperties],
+ // [EventListResponseEventMessagePartUpdatedProperties],
+ // [EventListResponseEventSessionUpdatedProperties],
+ // [EventListResponseEventSessionDeletedProperties],
+ // [EventListResponseEventSessionIdleProperties],
+ // [EventListResponseEventSessionErrorProperties],
+ // [EventListResponseEventFileWatcherUpdatedProperties].
+ Properties interface{} `json:"properties,required"`
+ Type EventListResponseType `json:"type,required"`
+ JSON eventListResponseJSON `json:"-"`
+ union EventListResponseUnion
+}
+
+// eventListResponseJSON contains the JSON metadata for the struct
+// [EventListResponse]
+type eventListResponseJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r eventListResponseJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *EventListResponse) UnmarshalJSON(data []byte) (err error) {
+ *r = EventListResponse{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [EventListResponseUnion] interface which you can cast to the
+// specific types for more type safety.
+//
+// Possible runtime types of the union are
+// [EventListResponseEventLspClientDiagnostics],
+// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
+// [EventListResponseEventStorageWrite],
+// [EventListResponseEventInstallationUpdated],
+// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
+// [EventListResponseEventMessagePartUpdated],
+// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
+// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError],
+// [EventListResponseEventFileWatcherUpdated].
+func (r EventListResponse) AsUnion() EventListResponseUnion {
+ return r.union
+}
+
+// Union satisfied by [EventListResponseEventLspClientDiagnostics],
+// [EventListResponseEventPermissionUpdated], [EventListResponseEventFileEdited],
+// [EventListResponseEventStorageWrite],
+// [EventListResponseEventInstallationUpdated],
+// [EventListResponseEventMessageUpdated], [EventListResponseEventMessageRemoved],
+// [EventListResponseEventMessagePartUpdated],
+// [EventListResponseEventSessionUpdated], [EventListResponseEventSessionDeleted],
+// [EventListResponseEventSessionIdle], [EventListResponseEventSessionError] or
+// [EventListResponseEventFileWatcherUpdated].
+type EventListResponseUnion interface {
+ implementsEventListResponse()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*EventListResponseUnion)(nil)).Elem(),
+ "type",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventLspClientDiagnostics{}),
+ DiscriminatorValue: "lsp.client.diagnostics",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventPermissionUpdated{}),
+ DiscriminatorValue: "permission.updated",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventFileEdited{}),
+ DiscriminatorValue: "file.edited",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventStorageWrite{}),
+ DiscriminatorValue: "storage.write",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventInstallationUpdated{}),
+ DiscriminatorValue: "installation.updated",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventMessageUpdated{}),
+ DiscriminatorValue: "message.updated",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventMessageRemoved{}),
+ DiscriminatorValue: "message.removed",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventMessagePartUpdated{}),
+ DiscriminatorValue: "message.part.updated",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventSessionUpdated{}),
+ DiscriminatorValue: "session.updated",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventSessionDeleted{}),
+ DiscriminatorValue: "session.deleted",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventSessionIdle{}),
+ DiscriminatorValue: "session.idle",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventSessionError{}),
+ DiscriminatorValue: "session.error",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventFileWatcherUpdated{}),
+ DiscriminatorValue: "file.watcher.updated",
+ },
+ )
+}
+
+type EventListResponseEventLspClientDiagnostics struct {
+ Properties EventListResponseEventLspClientDiagnosticsProperties `json:"properties,required"`
+ Type EventListResponseEventLspClientDiagnosticsType `json:"type,required"`
+ JSON eventListResponseEventLspClientDiagnosticsJSON `json:"-"`
+}
+
+// eventListResponseEventLspClientDiagnosticsJSON contains the JSON metadata for
+// the struct [EventListResponseEventLspClientDiagnostics]
+type eventListResponseEventLspClientDiagnosticsJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventLspClientDiagnostics) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventLspClientDiagnosticsJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventLspClientDiagnostics) implementsEventListResponse() {}
+
+type EventListResponseEventLspClientDiagnosticsProperties struct {
+ Path string `json:"path,required"`
+ ServerID string `json:"serverID,required"`
+ JSON eventListResponseEventLspClientDiagnosticsPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventLspClientDiagnosticsPropertiesJSON contains the JSON
+// metadata for the struct [EventListResponseEventLspClientDiagnosticsProperties]
+type eventListResponseEventLspClientDiagnosticsPropertiesJSON struct {
+ Path apijson.Field
+ ServerID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventLspClientDiagnosticsProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventLspClientDiagnosticsPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventLspClientDiagnosticsType string
+
+const (
+ EventListResponseEventLspClientDiagnosticsTypeLspClientDiagnostics EventListResponseEventLspClientDiagnosticsType = "lsp.client.diagnostics"
+)
+
+func (r EventListResponseEventLspClientDiagnosticsType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventLspClientDiagnosticsTypeLspClientDiagnostics:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventPermissionUpdated struct {
+ Properties EventListResponseEventPermissionUpdatedProperties `json:"properties,required"`
+ Type EventListResponseEventPermissionUpdatedType `json:"type,required"`
+ JSON eventListResponseEventPermissionUpdatedJSON `json:"-"`
+}
+
+// eventListResponseEventPermissionUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventPermissionUpdated]
+type eventListResponseEventPermissionUpdatedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventPermissionUpdated) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventPermissionUpdatedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventPermissionUpdated) implementsEventListResponse() {}
+
+type EventListResponseEventPermissionUpdatedProperties struct {
+ ID string `json:"id,required"`
+ Metadata map[string]interface{} `json:"metadata,required"`
+ SessionID string `json:"sessionID,required"`
+ Time EventListResponseEventPermissionUpdatedPropertiesTime `json:"time,required"`
+ Title string `json:"title,required"`
+ JSON eventListResponseEventPermissionUpdatedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventPermissionUpdatedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventPermissionUpdatedProperties]
+type eventListResponseEventPermissionUpdatedPropertiesJSON struct {
+ ID apijson.Field
+ Metadata apijson.Field
+ SessionID apijson.Field
+ Time apijson.Field
+ Title apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventPermissionUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventPermissionUpdatedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventPermissionUpdatedPropertiesTime struct {
+ Created float64 `json:"created,required"`
+ JSON eventListResponseEventPermissionUpdatedPropertiesTimeJSON `json:"-"`
+}
+
+// eventListResponseEventPermissionUpdatedPropertiesTimeJSON contains the JSON
+// metadata for the struct [EventListResponseEventPermissionUpdatedPropertiesTime]
+type eventListResponseEventPermissionUpdatedPropertiesTimeJSON struct {
+ Created apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventPermissionUpdatedPropertiesTime) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventPermissionUpdatedPropertiesTimeJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventPermissionUpdatedType string
+
+const (
+ EventListResponseEventPermissionUpdatedTypePermissionUpdated EventListResponseEventPermissionUpdatedType = "permission.updated"
+)
+
+func (r EventListResponseEventPermissionUpdatedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventPermissionUpdatedTypePermissionUpdated:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventFileEdited struct {
+ Properties EventListResponseEventFileEditedProperties `json:"properties,required"`
+ Type EventListResponseEventFileEditedType `json:"type,required"`
+ JSON eventListResponseEventFileEditedJSON `json:"-"`
+}
+
+// eventListResponseEventFileEditedJSON contains the JSON metadata for the struct
+// [EventListResponseEventFileEdited]
+type eventListResponseEventFileEditedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventFileEdited) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventFileEditedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventFileEdited) implementsEventListResponse() {}
+
+type EventListResponseEventFileEditedProperties struct {
+ File string `json:"file,required"`
+ JSON eventListResponseEventFileEditedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventFileEditedPropertiesJSON contains the JSON metadata for
+// the struct [EventListResponseEventFileEditedProperties]
+type eventListResponseEventFileEditedPropertiesJSON struct {
+ File apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventFileEditedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventFileEditedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventFileEditedType string
+
+const (
+ EventListResponseEventFileEditedTypeFileEdited EventListResponseEventFileEditedType = "file.edited"
+)
+
+func (r EventListResponseEventFileEditedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventFileEditedTypeFileEdited:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventStorageWrite struct {
+ Properties EventListResponseEventStorageWriteProperties `json:"properties,required"`
+ Type EventListResponseEventStorageWriteType `json:"type,required"`
+ JSON eventListResponseEventStorageWriteJSON `json:"-"`
+}
+
+// eventListResponseEventStorageWriteJSON contains the JSON metadata for the struct
+// [EventListResponseEventStorageWrite]
+type eventListResponseEventStorageWriteJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventStorageWrite) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventStorageWriteJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventStorageWrite) implementsEventListResponse() {}
+
+type EventListResponseEventStorageWriteProperties struct {
+ Key string `json:"key,required"`
+ Content interface{} `json:"content"`
+ JSON eventListResponseEventStorageWritePropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventStorageWritePropertiesJSON contains the JSON metadata for
+// the struct [EventListResponseEventStorageWriteProperties]
+type eventListResponseEventStorageWritePropertiesJSON struct {
+ Key apijson.Field
+ Content apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventStorageWriteProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventStorageWritePropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventStorageWriteType string
+
+const (
+ EventListResponseEventStorageWriteTypeStorageWrite EventListResponseEventStorageWriteType = "storage.write"
+)
+
+func (r EventListResponseEventStorageWriteType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventStorageWriteTypeStorageWrite:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventInstallationUpdated struct {
+ Properties EventListResponseEventInstallationUpdatedProperties `json:"properties,required"`
+ Type EventListResponseEventInstallationUpdatedType `json:"type,required"`
+ JSON eventListResponseEventInstallationUpdatedJSON `json:"-"`
+}
+
+// eventListResponseEventInstallationUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventInstallationUpdated]
+type eventListResponseEventInstallationUpdatedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventInstallationUpdated) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventInstallationUpdatedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventInstallationUpdated) implementsEventListResponse() {}
+
+type EventListResponseEventInstallationUpdatedProperties struct {
+ Version string `json:"version,required"`
+ JSON eventListResponseEventInstallationUpdatedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventInstallationUpdatedPropertiesJSON contains the JSON
+// metadata for the struct [EventListResponseEventInstallationUpdatedProperties]
+type eventListResponseEventInstallationUpdatedPropertiesJSON struct {
+ Version apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventInstallationUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventInstallationUpdatedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventInstallationUpdatedType string
+
+const (
+ EventListResponseEventInstallationUpdatedTypeInstallationUpdated EventListResponseEventInstallationUpdatedType = "installation.updated"
+)
+
+func (r EventListResponseEventInstallationUpdatedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventInstallationUpdatedTypeInstallationUpdated:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventMessageUpdated struct {
+ Properties EventListResponseEventMessageUpdatedProperties `json:"properties,required"`
+ Type EventListResponseEventMessageUpdatedType `json:"type,required"`
+ JSON eventListResponseEventMessageUpdatedJSON `json:"-"`
+}
+
+// eventListResponseEventMessageUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventMessageUpdated]
+type eventListResponseEventMessageUpdatedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventMessageUpdated) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventMessageUpdatedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventMessageUpdated) implementsEventListResponse() {}
+
+type EventListResponseEventMessageUpdatedProperties struct {
+ Info Message `json:"info,required"`
+ JSON eventListResponseEventMessageUpdatedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventMessageUpdatedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventMessageUpdatedProperties]
+type eventListResponseEventMessageUpdatedPropertiesJSON struct {
+ Info apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventMessageUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventMessageUpdatedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventMessageUpdatedType string
+
+const (
+ EventListResponseEventMessageUpdatedTypeMessageUpdated EventListResponseEventMessageUpdatedType = "message.updated"
+)
+
+func (r EventListResponseEventMessageUpdatedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventMessageUpdatedTypeMessageUpdated:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventMessageRemoved struct {
+ Properties EventListResponseEventMessageRemovedProperties `json:"properties,required"`
+ Type EventListResponseEventMessageRemovedType `json:"type,required"`
+ JSON eventListResponseEventMessageRemovedJSON `json:"-"`
+}
+
+// eventListResponseEventMessageRemovedJSON contains the JSON metadata for the
+// struct [EventListResponseEventMessageRemoved]
+type eventListResponseEventMessageRemovedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventMessageRemoved) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventMessageRemovedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventMessageRemoved) implementsEventListResponse() {}
+
+type EventListResponseEventMessageRemovedProperties struct {
+ MessageID string `json:"messageID,required"`
+ SessionID string `json:"sessionID,required"`
+ JSON eventListResponseEventMessageRemovedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventMessageRemovedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventMessageRemovedProperties]
+type eventListResponseEventMessageRemovedPropertiesJSON struct {
+ MessageID apijson.Field
+ SessionID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventMessageRemovedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventMessageRemovedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventMessageRemovedType string
+
+const (
+ EventListResponseEventMessageRemovedTypeMessageRemoved EventListResponseEventMessageRemovedType = "message.removed"
+)
+
+func (r EventListResponseEventMessageRemovedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventMessageRemovedTypeMessageRemoved:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventMessagePartUpdated struct {
+ Properties EventListResponseEventMessagePartUpdatedProperties `json:"properties,required"`
+ Type EventListResponseEventMessagePartUpdatedType `json:"type,required"`
+ JSON eventListResponseEventMessagePartUpdatedJSON `json:"-"`
+}
+
+// eventListResponseEventMessagePartUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventMessagePartUpdated]
+type eventListResponseEventMessagePartUpdatedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventMessagePartUpdated) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventMessagePartUpdatedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventMessagePartUpdated) implementsEventListResponse() {}
+
+type EventListResponseEventMessagePartUpdatedProperties struct {
+ MessageID string `json:"messageID,required"`
+ Part MessagePart `json:"part,required"`
+ SessionID string `json:"sessionID,required"`
+ JSON eventListResponseEventMessagePartUpdatedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventMessagePartUpdatedPropertiesJSON contains the JSON
+// metadata for the struct [EventListResponseEventMessagePartUpdatedProperties]
+type eventListResponseEventMessagePartUpdatedPropertiesJSON struct {
+ MessageID apijson.Field
+ Part apijson.Field
+ SessionID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventMessagePartUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventMessagePartUpdatedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventMessagePartUpdatedType string
+
+const (
+ EventListResponseEventMessagePartUpdatedTypeMessagePartUpdated EventListResponseEventMessagePartUpdatedType = "message.part.updated"
+)
+
+func (r EventListResponseEventMessagePartUpdatedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventMessagePartUpdatedTypeMessagePartUpdated:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventSessionUpdated struct {
+ Properties EventListResponseEventSessionUpdatedProperties `json:"properties,required"`
+ Type EventListResponseEventSessionUpdatedType `json:"type,required"`
+ JSON eventListResponseEventSessionUpdatedJSON `json:"-"`
+}
+
+// eventListResponseEventSessionUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventSessionUpdated]
+type eventListResponseEventSessionUpdatedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionUpdated) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionUpdatedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventSessionUpdated) implementsEventListResponse() {}
+
+type EventListResponseEventSessionUpdatedProperties struct {
+ Info Session `json:"info,required"`
+ JSON eventListResponseEventSessionUpdatedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventSessionUpdatedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventSessionUpdatedProperties]
+type eventListResponseEventSessionUpdatedPropertiesJSON struct {
+ Info apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionUpdatedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventSessionUpdatedType string
+
+const (
+ EventListResponseEventSessionUpdatedTypeSessionUpdated EventListResponseEventSessionUpdatedType = "session.updated"
+)
+
+func (r EventListResponseEventSessionUpdatedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventSessionUpdatedTypeSessionUpdated:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventSessionDeleted struct {
+ Properties EventListResponseEventSessionDeletedProperties `json:"properties,required"`
+ Type EventListResponseEventSessionDeletedType `json:"type,required"`
+ JSON eventListResponseEventSessionDeletedJSON `json:"-"`
+}
+
+// eventListResponseEventSessionDeletedJSON contains the JSON metadata for the
+// struct [EventListResponseEventSessionDeleted]
+type eventListResponseEventSessionDeletedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionDeleted) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionDeletedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventSessionDeleted) implementsEventListResponse() {}
+
+type EventListResponseEventSessionDeletedProperties struct {
+ Info Session `json:"info,required"`
+ JSON eventListResponseEventSessionDeletedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventSessionDeletedPropertiesJSON contains the JSON metadata
+// for the struct [EventListResponseEventSessionDeletedProperties]
+type eventListResponseEventSessionDeletedPropertiesJSON struct {
+ Info apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionDeletedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionDeletedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventSessionDeletedType string
+
+const (
+ EventListResponseEventSessionDeletedTypeSessionDeleted EventListResponseEventSessionDeletedType = "session.deleted"
+)
+
+func (r EventListResponseEventSessionDeletedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventSessionDeletedTypeSessionDeleted:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventSessionIdle struct {
+ Properties EventListResponseEventSessionIdleProperties `json:"properties,required"`
+ Type EventListResponseEventSessionIdleType `json:"type,required"`
+ JSON eventListResponseEventSessionIdleJSON `json:"-"`
+}
+
+// eventListResponseEventSessionIdleJSON contains the JSON metadata for the struct
+// [EventListResponseEventSessionIdle]
+type eventListResponseEventSessionIdleJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionIdle) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionIdleJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventSessionIdle) implementsEventListResponse() {}
+
+type EventListResponseEventSessionIdleProperties struct {
+ SessionID string `json:"sessionID,required"`
+ JSON eventListResponseEventSessionIdlePropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventSessionIdlePropertiesJSON contains the JSON metadata for
+// the struct [EventListResponseEventSessionIdleProperties]
+type eventListResponseEventSessionIdlePropertiesJSON struct {
+ SessionID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionIdleProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionIdlePropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventSessionIdleType string
+
+const (
+ EventListResponseEventSessionIdleTypeSessionIdle EventListResponseEventSessionIdleType = "session.idle"
+)
+
+func (r EventListResponseEventSessionIdleType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventSessionIdleTypeSessionIdle:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventSessionError struct {
+ Properties EventListResponseEventSessionErrorProperties `json:"properties,required"`
+ Type EventListResponseEventSessionErrorType `json:"type,required"`
+ JSON eventListResponseEventSessionErrorJSON `json:"-"`
+}
+
+// eventListResponseEventSessionErrorJSON contains the JSON metadata for the struct
+// [EventListResponseEventSessionError]
+type eventListResponseEventSessionErrorJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionError) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventSessionError) implementsEventListResponse() {}
+
+type EventListResponseEventSessionErrorProperties struct {
+ Error EventListResponseEventSessionErrorPropertiesError `json:"error"`
+ JSON eventListResponseEventSessionErrorPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventSessionErrorPropertiesJSON contains the JSON metadata for
+// the struct [EventListResponseEventSessionErrorProperties]
+type eventListResponseEventSessionErrorPropertiesJSON struct {
+ Error apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionErrorProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionErrorPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventSessionErrorPropertiesError struct {
+ // This field can have the runtime type of [shared.ProviderAuthErrorData],
+ // [shared.UnknownErrorData], [interface{}].
+ Data interface{} `json:"data,required"`
+ Name EventListResponseEventSessionErrorPropertiesErrorName `json:"name,required"`
+ JSON eventListResponseEventSessionErrorPropertiesErrorJSON `json:"-"`
+ union EventListResponseEventSessionErrorPropertiesErrorUnion
+}
+
+// eventListResponseEventSessionErrorPropertiesErrorJSON contains the JSON metadata
+// for the struct [EventListResponseEventSessionErrorPropertiesError]
+type eventListResponseEventSessionErrorPropertiesErrorJSON struct {
+ Data apijson.Field
+ Name apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r eventListResponseEventSessionErrorPropertiesErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *EventListResponseEventSessionErrorPropertiesError) UnmarshalJSON(data []byte) (err error) {
+ *r = EventListResponseEventSessionErrorPropertiesError{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [EventListResponseEventSessionErrorPropertiesErrorUnion]
+// interface which you can cast to the specific types for more type safety.
+//
+// Possible runtime types of the union are [shared.ProviderAuthError],
+// [shared.UnknownError],
+// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError].
+func (r EventListResponseEventSessionErrorPropertiesError) AsUnion() EventListResponseEventSessionErrorPropertiesErrorUnion {
+ return r.union
+}
+
+// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or
+// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError].
+type EventListResponseEventSessionErrorPropertiesErrorUnion interface {
+ ImplementsEventListResponseEventSessionErrorPropertiesError()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*EventListResponseEventSessionErrorPropertiesErrorUnion)(nil)).Elem(),
+ "name",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(shared.ProviderAuthError{}),
+ DiscriminatorValue: "ProviderAuthError",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(shared.UnknownError{}),
+ DiscriminatorValue: "UnknownError",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError{}),
+ DiscriminatorValue: "MessageOutputLengthError",
+ },
+ )
+}
+
+type EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError struct {
+ Data interface{} `json:"data,required"`
+ Name EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName `json:"name,required"`
+ JSON eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON `json:"-"`
+}
+
+// eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON
+// contains the JSON metadata for the struct
+// [EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError]
+type eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON struct {
+ Data apijson.Field
+ Name apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {
+}
+
+type EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName string
+
+const (
+ EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName = "MessageOutputLengthError"
+)
+
+func (r EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorName) IsKnown() bool {
+ switch r {
+ case EventListResponseEventSessionErrorPropertiesErrorMessageOutputLengthErrorNameMessageOutputLengthError:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventSessionErrorPropertiesErrorName string
+
+const (
+ EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError EventListResponseEventSessionErrorPropertiesErrorName = "ProviderAuthError"
+ EventListResponseEventSessionErrorPropertiesErrorNameUnknownError EventListResponseEventSessionErrorPropertiesErrorName = "UnknownError"
+ EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError EventListResponseEventSessionErrorPropertiesErrorName = "MessageOutputLengthError"
+)
+
+func (r EventListResponseEventSessionErrorPropertiesErrorName) IsKnown() bool {
+ switch r {
+ case EventListResponseEventSessionErrorPropertiesErrorNameProviderAuthError, EventListResponseEventSessionErrorPropertiesErrorNameUnknownError, EventListResponseEventSessionErrorPropertiesErrorNameMessageOutputLengthError:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventSessionErrorType string
+
+const (
+ EventListResponseEventSessionErrorTypeSessionError EventListResponseEventSessionErrorType = "session.error"
+)
+
+func (r EventListResponseEventSessionErrorType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventSessionErrorTypeSessionError:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventFileWatcherUpdated struct {
+ Properties EventListResponseEventFileWatcherUpdatedProperties `json:"properties,required"`
+ Type EventListResponseEventFileWatcherUpdatedType `json:"type,required"`
+ JSON eventListResponseEventFileWatcherUpdatedJSON `json:"-"`
+}
+
+// eventListResponseEventFileWatcherUpdatedJSON contains the JSON metadata for the
+// struct [EventListResponseEventFileWatcherUpdated]
+type eventListResponseEventFileWatcherUpdatedJSON struct {
+ Properties apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventFileWatcherUpdated) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventFileWatcherUpdatedJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r EventListResponseEventFileWatcherUpdated) implementsEventListResponse() {}
+
+type EventListResponseEventFileWatcherUpdatedProperties struct {
+ Event EventListResponseEventFileWatcherUpdatedPropertiesEvent `json:"event,required"`
+ File string `json:"file,required"`
+ JSON eventListResponseEventFileWatcherUpdatedPropertiesJSON `json:"-"`
+}
+
+// eventListResponseEventFileWatcherUpdatedPropertiesJSON contains the JSON
+// metadata for the struct [EventListResponseEventFileWatcherUpdatedProperties]
+type eventListResponseEventFileWatcherUpdatedPropertiesJSON struct {
+ Event apijson.Field
+ File apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *EventListResponseEventFileWatcherUpdatedProperties) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r eventListResponseEventFileWatcherUpdatedPropertiesJSON) RawJSON() string {
+ return r.raw
+}
+
+type EventListResponseEventFileWatcherUpdatedPropertiesEvent string
+
+const (
+ EventListResponseEventFileWatcherUpdatedPropertiesEventRename EventListResponseEventFileWatcherUpdatedPropertiesEvent = "rename"
+ EventListResponseEventFileWatcherUpdatedPropertiesEventChange EventListResponseEventFileWatcherUpdatedPropertiesEvent = "change"
+)
+
+func (r EventListResponseEventFileWatcherUpdatedPropertiesEvent) IsKnown() bool {
+ switch r {
+ case EventListResponseEventFileWatcherUpdatedPropertiesEventRename, EventListResponseEventFileWatcherUpdatedPropertiesEventChange:
+ return true
+ }
+ return false
+}
+
+type EventListResponseEventFileWatcherUpdatedType string
+
+const (
+ EventListResponseEventFileWatcherUpdatedTypeFileWatcherUpdated EventListResponseEventFileWatcherUpdatedType = "file.watcher.updated"
+)
+
+func (r EventListResponseEventFileWatcherUpdatedType) IsKnown() bool {
+ switch r {
+ case EventListResponseEventFileWatcherUpdatedTypeFileWatcherUpdated:
+ return true
+ }
+ return false
+}
+
+type EventListResponseType string
+
+const (
+ EventListResponseTypeLspClientDiagnostics EventListResponseType = "lsp.client.diagnostics"
+ EventListResponseTypePermissionUpdated EventListResponseType = "permission.updated"
+ EventListResponseTypeFileEdited EventListResponseType = "file.edited"
+ EventListResponseTypeStorageWrite EventListResponseType = "storage.write"
+ EventListResponseTypeInstallationUpdated EventListResponseType = "installation.updated"
+ EventListResponseTypeMessageUpdated EventListResponseType = "message.updated"
+ EventListResponseTypeMessageRemoved EventListResponseType = "message.removed"
+ EventListResponseTypeMessagePartUpdated EventListResponseType = "message.part.updated"
+ EventListResponseTypeSessionUpdated EventListResponseType = "session.updated"
+ EventListResponseTypeSessionDeleted EventListResponseType = "session.deleted"
+ EventListResponseTypeSessionIdle EventListResponseType = "session.idle"
+ EventListResponseTypeSessionError EventListResponseType = "session.error"
+ EventListResponseTypeFileWatcherUpdated EventListResponseType = "file.watcher.updated"
+)
+
+func (r EventListResponseType) IsKnown() bool {
+ switch r {
+ case EventListResponseTypeLspClientDiagnostics, EventListResponseTypePermissionUpdated, EventListResponseTypeFileEdited, EventListResponseTypeStorageWrite, EventListResponseTypeInstallationUpdated, EventListResponseTypeMessageUpdated, EventListResponseTypeMessageRemoved, EventListResponseTypeMessagePartUpdated, EventListResponseTypeSessionUpdated, EventListResponseTypeSessionDeleted, EventListResponseTypeSessionIdle, EventListResponseTypeSessionError, EventListResponseTypeFileWatcherUpdated:
+ return true
+ }
+ return false
+}
diff --git a/packages/tui/sdk/examples/.keep b/packages/tui/sdk/examples/.keep
new file mode 100644
index 000000000..d8c73e937
--- /dev/null
+++ b/packages/tui/sdk/examples/.keep
@@ -0,0 +1,4 @@
+File generated from our OpenAPI spec by Stainless.
+
+This directory can be used to store example files demonstrating usage of this SDK.
+It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file
diff --git a/packages/tui/sdk/field.go b/packages/tui/sdk/field.go
new file mode 100644
index 000000000..56d2f8903
--- /dev/null
+++ b/packages/tui/sdk/field.go
@@ -0,0 +1,50 @@
+package opencode
+
+import (
+ "github.com/sst/opencode-sdk-go/internal/param"
+ "io"
+)
+
+// F is a param field helper used to initialize a [param.Field] generic struct.
+// This helps specify null, zero values, and overrides, as well as normal values.
+// You can read more about this in our [README].
+//
+// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-request-fields
+func F[T any](value T) param.Field[T] { return param.Field[T]{Value: value, Present: true} }
+
+// Null is a param field helper which explicitly sends null to the API.
+func Null[T any]() param.Field[T] { return param.Field[T]{Null: true, Present: true} }
+
+// Raw is a param field helper for specifying values for fields when the
+// type you are looking to send is different from the type that is specified in
+// the SDK. For example, if the type of the field is an integer, but you want
+// to send a float, you could do that by setting the corresponding field with
+// Raw[int](0.5).
+func Raw[T any](value any) param.Field[T] { return param.Field[T]{Raw: value, Present: true} }
+
+// Int is a param field helper which helps specify integers. This is
+// particularly helpful when specifying integer constants for fields.
+func Int(value int64) param.Field[int64] { return F(value) }
+
+// String is a param field helper which helps specify strings.
+func String(value string) param.Field[string] { return F(value) }
+
+// Float is a param field helper which helps specify floats.
+func Float(value float64) param.Field[float64] { return F(value) }
+
+// Bool is a param field helper which helps specify bools.
+func Bool(value bool) param.Field[bool] { return F(value) }
+
+// FileParam is a param field helper which helps files with a mime content-type.
+func FileParam(reader io.Reader, filename string, contentType string) param.Field[io.Reader] {
+ return F[io.Reader](&file{reader, filename, contentType})
+}
+
+type file struct {
+ io.Reader
+ name string
+ contentType string
+}
+
+func (f *file) ContentType() string { return f.contentType }
+func (f *file) Filename() string { return f.name }
diff --git a/packages/tui/sdk/file.go b/packages/tui/sdk/file.go
new file mode 100644
index 000000000..a9d6f018c
--- /dev/null
+++ b/packages/tui/sdk/file.go
@@ -0,0 +1,143 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+ "github.com/sst/opencode-sdk-go/internal/apiquery"
+ "github.com/sst/opencode-sdk-go/internal/param"
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+// FileService 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 [NewFileService] method instead.
+type FileService struct {
+ Options []option.RequestOption
+}
+
+// NewFileService 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 NewFileService(opts ...option.RequestOption) (r *FileService) {
+ r = &FileService{}
+ r.Options = opts
+ return
+}
+
+// Read a file
+func (r *FileService) Read(ctx context.Context, query FileReadParams, opts ...option.RequestOption) (res *FileReadResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "file"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
+ return
+}
+
+// Get file status
+func (r *FileService) Status(ctx context.Context, opts ...option.RequestOption) (res *[]FileStatusResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "file/status"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+type FileReadResponse struct {
+ Content string `json:"content,required"`
+ Type FileReadResponseType `json:"type,required"`
+ JSON fileReadResponseJSON `json:"-"`
+}
+
+// fileReadResponseJSON contains the JSON metadata for the struct
+// [FileReadResponse]
+type fileReadResponseJSON struct {
+ Content apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FileReadResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r fileReadResponseJSON) RawJSON() string {
+ return r.raw
+}
+
+type FileReadResponseType string
+
+const (
+ FileReadResponseTypeRaw FileReadResponseType = "raw"
+ FileReadResponseTypePatch FileReadResponseType = "patch"
+)
+
+func (r FileReadResponseType) IsKnown() bool {
+ switch r {
+ case FileReadResponseTypeRaw, FileReadResponseTypePatch:
+ return true
+ }
+ return false
+}
+
+type FileStatusResponse struct {
+ Added int64 `json:"added,required"`
+ File string `json:"file,required"`
+ Removed int64 `json:"removed,required"`
+ Status FileStatusResponseStatus `json:"status,required"`
+ JSON fileStatusResponseJSON `json:"-"`
+}
+
+// fileStatusResponseJSON contains the JSON metadata for the struct
+// [FileStatusResponse]
+type fileStatusResponseJSON struct {
+ Added apijson.Field
+ File apijson.Field
+ Removed apijson.Field
+ Status apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FileStatusResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r fileStatusResponseJSON) RawJSON() string {
+ return r.raw
+}
+
+type FileStatusResponseStatus string
+
+const (
+ FileStatusResponseStatusAdded FileStatusResponseStatus = "added"
+ FileStatusResponseStatusDeleted FileStatusResponseStatus = "deleted"
+ FileStatusResponseStatusModified FileStatusResponseStatus = "modified"
+)
+
+func (r FileStatusResponseStatus) IsKnown() bool {
+ switch r {
+ case FileStatusResponseStatusAdded, FileStatusResponseStatusDeleted, FileStatusResponseStatusModified:
+ return true
+ }
+ return false
+}
+
+type FileReadParams struct {
+ Path param.Field[string] `query:"path,required"`
+}
+
+// URLQuery serializes [FileReadParams]'s query parameters as `url.Values`.
+func (r FileReadParams) URLQuery() (v url.Values) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
diff --git a/packages/tui/sdk/file_test.go b/packages/tui/sdk/file_test.go
new file mode 100644
index 000000000..60212ea24
--- /dev/null
+++ b/packages/tui/sdk/file_test.go
@@ -0,0 +1,60 @@
+// 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 TestFileRead(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.File.Read(context.TODO(), opencode.FileReadParams{
+ Path: opencode.F("path"),
+ })
+ 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 TestFileStatus(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.File.Status(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/sdk/find.go b/packages/tui/sdk/find.go
new file mode 100644
index 000000000..bbd6b680b
--- /dev/null
+++ b/packages/tui/sdk/find.go
@@ -0,0 +1,213 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+ "github.com/sst/opencode-sdk-go/internal/apiquery"
+ "github.com/sst/opencode-sdk-go/internal/param"
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+// FindService 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 [NewFindService] method instead.
+type FindService struct {
+ Options []option.RequestOption
+}
+
+// NewFindService 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 NewFindService(opts ...option.RequestOption) (r *FindService) {
+ r = &FindService{}
+ r.Options = opts
+ return
+}
+
+// Find files
+func (r *FindService) Files(ctx context.Context, query FindFilesParams, opts ...option.RequestOption) (res *[]string, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "find/file"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
+ return
+}
+
+// Find workspace symbols
+func (r *FindService) Symbols(ctx context.Context, query FindSymbolsParams, opts ...option.RequestOption) (res *[]FindSymbolsResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "find/symbol"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
+ return
+}
+
+// Find text in files
+func (r *FindService) Text(ctx context.Context, query FindTextParams, opts ...option.RequestOption) (res *[]FindTextResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "find"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...)
+ return
+}
+
+type FindSymbolsResponse = interface{}
+
+type FindTextResponse struct {
+ AbsoluteOffset float64 `json:"absolute_offset,required"`
+ LineNumber float64 `json:"line_number,required"`
+ Lines FindTextResponseLines `json:"lines,required"`
+ Path FindTextResponsePath `json:"path,required"`
+ Submatches []FindTextResponseSubmatch `json:"submatches,required"`
+ JSON findTextResponseJSON `json:"-"`
+}
+
+// findTextResponseJSON contains the JSON metadata for the struct
+// [FindTextResponse]
+type findTextResponseJSON struct {
+ AbsoluteOffset apijson.Field
+ LineNumber apijson.Field
+ Lines apijson.Field
+ Path apijson.Field
+ Submatches apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FindTextResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r findTextResponseJSON) RawJSON() string {
+ return r.raw
+}
+
+type FindTextResponseLines struct {
+ Text string `json:"text,required"`
+ JSON findTextResponseLinesJSON `json:"-"`
+}
+
+// findTextResponseLinesJSON contains the JSON metadata for the struct
+// [FindTextResponseLines]
+type findTextResponseLinesJSON struct {
+ Text apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FindTextResponseLines) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r findTextResponseLinesJSON) RawJSON() string {
+ return r.raw
+}
+
+type FindTextResponsePath struct {
+ Text string `json:"text,required"`
+ JSON findTextResponsePathJSON `json:"-"`
+}
+
+// findTextResponsePathJSON contains the JSON metadata for the struct
+// [FindTextResponsePath]
+type findTextResponsePathJSON struct {
+ Text apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FindTextResponsePath) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r findTextResponsePathJSON) RawJSON() string {
+ return r.raw
+}
+
+type FindTextResponseSubmatch struct {
+ End float64 `json:"end,required"`
+ Match FindTextResponseSubmatchesMatch `json:"match,required"`
+ Start float64 `json:"start,required"`
+ JSON findTextResponseSubmatchJSON `json:"-"`
+}
+
+// findTextResponseSubmatchJSON contains the JSON metadata for the struct
+// [FindTextResponseSubmatch]
+type findTextResponseSubmatchJSON struct {
+ End apijson.Field
+ Match apijson.Field
+ Start apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FindTextResponseSubmatch) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r findTextResponseSubmatchJSON) RawJSON() string {
+ return r.raw
+}
+
+type FindTextResponseSubmatchesMatch struct {
+ Text string `json:"text,required"`
+ JSON findTextResponseSubmatchesMatchJSON `json:"-"`
+}
+
+// findTextResponseSubmatchesMatchJSON contains the JSON metadata for the struct
+// [FindTextResponseSubmatchesMatch]
+type findTextResponseSubmatchesMatchJSON struct {
+ Text apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FindTextResponseSubmatchesMatch) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r findTextResponseSubmatchesMatchJSON) RawJSON() string {
+ return r.raw
+}
+
+type FindFilesParams struct {
+ Query param.Field[string] `query:"query,required"`
+}
+
+// URLQuery serializes [FindFilesParams]'s query parameters as `url.Values`.
+func (r FindFilesParams) URLQuery() (v url.Values) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
+
+type FindSymbolsParams struct {
+ Query param.Field[string] `query:"query,required"`
+}
+
+// URLQuery serializes [FindSymbolsParams]'s query parameters as `url.Values`.
+func (r FindSymbolsParams) URLQuery() (v url.Values) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
+
+type FindTextParams struct {
+ Pattern param.Field[string] `query:"pattern,required"`
+}
+
+// URLQuery serializes [FindTextParams]'s query parameters as `url.Values`.
+func (r FindTextParams) URLQuery() (v url.Values) {
+ return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{
+ ArrayFormat: apiquery.ArrayQueryFormatComma,
+ NestedFormat: apiquery.NestedQueryFormatBrackets,
+ })
+}
diff --git a/packages/tui/sdk/find_test.go b/packages/tui/sdk/find_test.go
new file mode 100644
index 000000000..e2f1caa16
--- /dev/null
+++ b/packages/tui/sdk/find_test.go
@@ -0,0 +1,86 @@
+// 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 TestFindFiles(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.Find.Files(context.TODO(), opencode.FindFilesParams{
+ Query: opencode.F("query"),
+ })
+ 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 TestFindSymbols(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.Find.Symbols(context.TODO(), opencode.FindSymbolsParams{
+ Query: opencode.F("query"),
+ })
+ 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 TestFindText(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.Find.Text(context.TODO(), opencode.FindTextParams{
+ Pattern: opencode.F("pattern"),
+ })
+ 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/sdk/go.mod b/packages/tui/sdk/go.mod
new file mode 100644
index 000000000..2817d3013
--- /dev/null
+++ b/packages/tui/sdk/go.mod
@@ -0,0 +1,13 @@
+module github.com/sst/opencode-sdk-go
+
+go 1.21
+
+require (
+ github.com/tidwall/gjson v1.14.4
+ github.com/tidwall/sjson v1.2.5
+)
+
+require (
+ github.com/tidwall/match v1.1.1 // indirect
+ github.com/tidwall/pretty v1.2.1 // indirect
+)
diff --git a/packages/tui/sdk/go.sum b/packages/tui/sdk/go.sum
new file mode 100644
index 000000000..a70a5e0a8
--- /dev/null
+++ b/packages/tui/sdk/go.sum
@@ -0,0 +1,10 @@
+github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
diff --git a/packages/tui/sdk/internal/apierror/apierror.go b/packages/tui/sdk/internal/apierror/apierror.go
new file mode 100644
index 000000000..24307fc36
--- /dev/null
+++ b/packages/tui/sdk/internal/apierror/apierror.go
@@ -0,0 +1,53 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package apierror
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httputil"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+)
+
+// Error represents an error that originates from the API, i.e. when a request is
+// made and the API returns a response with a HTTP status code. Other errors are
+// not wrapped by this SDK.
+type Error struct {
+ JSON errorJSON `json:"-"`
+ StatusCode int
+ Request *http.Request
+ Response *http.Response
+}
+
+// errorJSON contains the JSON metadata for the struct [Error]
+type errorJSON struct {
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Error) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r errorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *Error) Error() string {
+ // Attempt to re-populate the response body
+ return fmt.Sprintf("%s \"%s\": %d %s %s", r.Request.Method, r.Request.URL, r.Response.StatusCode, http.StatusText(r.Response.StatusCode), r.JSON.RawJSON())
+}
+
+func (r *Error) DumpRequest(body bool) []byte {
+ if r.Request.GetBody != nil {
+ r.Request.Body, _ = r.Request.GetBody()
+ }
+ out, _ := httputil.DumpRequestOut(r.Request, body)
+ return out
+}
+
+func (r *Error) DumpResponse(body bool) []byte {
+ out, _ := httputil.DumpResponse(r.Response, body)
+ return out
+}
diff --git a/packages/tui/sdk/internal/apiform/encoder.go b/packages/tui/sdk/internal/apiform/encoder.go
new file mode 100644
index 000000000..243a1a123
--- /dev/null
+++ b/packages/tui/sdk/internal/apiform/encoder.go
@@ -0,0 +1,383 @@
+package apiform
+
+import (
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/textproto"
+ "path"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/sst/opencode-sdk-go/internal/param"
+)
+
+var encoders sync.Map // map[encoderEntry]encoderFunc
+
+func Marshal(value interface{}, writer *multipart.Writer) error {
+ e := &encoder{dateFormat: time.RFC3339}
+ return e.marshal(value, writer)
+}
+
+func MarshalRoot(value interface{}, writer *multipart.Writer) error {
+ e := &encoder{root: true, dateFormat: time.RFC3339}
+ return e.marshal(value, writer)
+}
+
+type encoder struct {
+ dateFormat string
+ root bool
+}
+
+type encoderFunc func(key string, value reflect.Value, writer *multipart.Writer) error
+
+type encoderField struct {
+ tag parsedStructTag
+ fn encoderFunc
+ idx []int
+}
+
+type encoderEntry struct {
+ reflect.Type
+ dateFormat string
+ root bool
+}
+
+func (e *encoder) marshal(value interface{}, writer *multipart.Writer) error {
+ val := reflect.ValueOf(value)
+ if !val.IsValid() {
+ return nil
+ }
+ typ := val.Type()
+ enc := e.typeEncoder(typ)
+ return enc("", val, writer)
+}
+
+func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
+ entry := encoderEntry{
+ Type: t,
+ dateFormat: e.dateFormat,
+ root: e.root,
+ }
+
+ if fi, ok := encoders.Load(entry); ok {
+ return fi.(encoderFunc)
+ }
+
+ // To deal with recursive types, populate the map with an
+ // indirect func before we build it. This type waits on the
+ // real func (f) to be ready and then calls it. This indirect
+ // func is only used for recursive types.
+ var (
+ wg sync.WaitGroup
+ f encoderFunc
+ )
+ wg.Add(1)
+ fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value, writer *multipart.Writer) error {
+ wg.Wait()
+ return f(key, v, writer)
+ }))
+ if loaded {
+ return fi.(encoderFunc)
+ }
+
+ // Compute the real encoder and replace the indirect func with it.
+ f = e.newTypeEncoder(t)
+ wg.Done()
+ encoders.Store(entry, f)
+ return f
+}
+
+func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
+ if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
+ return e.newTimeTypeEncoder()
+ }
+ if t.ConvertibleTo(reflect.TypeOf((*io.Reader)(nil)).Elem()) {
+ return e.newReaderTypeEncoder()
+ }
+ e.root = false
+ switch t.Kind() {
+ case reflect.Pointer:
+ inner := t.Elem()
+
+ innerEncoder := e.typeEncoder(inner)
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ if !v.IsValid() || v.IsNil() {
+ return nil
+ }
+ return innerEncoder(key, v.Elem(), writer)
+ }
+ case reflect.Struct:
+ return e.newStructTypeEncoder(t)
+ case reflect.Slice, reflect.Array:
+ return e.newArrayTypeEncoder(t)
+ case reflect.Map:
+ return e.newMapEncoder(t)
+ case reflect.Interface:
+ return e.newInterfaceEncoder()
+ default:
+ return e.newPrimitiveTypeEncoder(t)
+ }
+}
+
+func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
+ switch t.Kind() {
+ // Note that we could use `gjson` to encode these types but it would complicate our
+ // code more and this current code shouldn't cause any issues
+ case reflect.String:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, v.String())
+ }
+ case reflect.Bool:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ if v.Bool() {
+ return writer.WriteField(key, "true")
+ }
+ return writer.WriteField(key, "false")
+ }
+ case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatInt(v.Int(), 10))
+ }
+ case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatUint(v.Uint(), 10))
+ }
+ case reflect.Float32:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 32))
+ }
+ case reflect.Float64:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, strconv.FormatFloat(v.Float(), 'f', -1, 64))
+ }
+ default:
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ return fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
+ }
+ }
+}
+
+func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
+ itemEncoder := e.typeEncoder(t.Elem())
+
+ return func(key string, v reflect.Value, writer *multipart.Writer) error {
+ if key != "" {
+ key = key + "."
+ }
+ for i := 0; i < v.Len(); i++ {
+ err := itemEncoder(key+strconv.Itoa(i), v.Index(i), writer)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+ }
+}
+
+func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
+ if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
+ return e.newFieldTypeEncoder(t)
+ }
+
+ encoderFields := []encoderField{}
+ extraEncoder := (*encoderField)(nil)
+
+ // This helper allows us to recursively collect field encoders into a flat
+ // array. The parameter `index` keeps track of the access patterns necessary
+ // to get to some field.
+ var collectEncoderFields func(r reflect.Type, index []int)
+ collectEncoderFields = func(r reflect.Type, index []int) {
+ for i := 0; i < r.NumField(); i++ {
+ idx := append(index, i)
+ field := t.FieldByIndex(idx)
+ if !field.IsExported() {
+ continue
+ }
+ // If this is an embedded struct, traverse one level deeper to extract
+ // the field and get their encoders as well.
+ if field.Anonymous {
+ collectEncoderFields(field.Type, idx)
+ continue
+ }
+ // If json tag is not present, then we skip, which is intentionally
+ // different behavior from the stdlib.
+ ptag, ok := parseFormStructTag(field)
+ if !ok {
+ continue
+ }
+ // We only want to support unexported field if they're tagged with
+ // `extras` because that field shouldn't be part of the public API. We
+ // also want to only keep the top level extras
+ if ptag.extras && len(index) == 0 {
+ extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
+ continue
+ }
+ if ptag.name == "-" {
+ continue
+ }
+
+ dateFormat, ok := parseFormatStructTag(field)
+ oldFormat := e.dateFormat
+ if ok {
+ switch dateFormat {
+ case "date-time":
+ e.dateFormat = time.RFC3339
+ case "date":
+ e.dateFormat = "2006-01-02"
+ }
+ }
+ encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
+ e.dateFormat = oldFormat
+ }
+ }
+ collectEncoderFields(t, []int{})
+
+ // Ensure deterministic output by sorting by lexicographic order
+ sort.Slice(encoderFields, func(i, j int) bool {
+ return encoderFields[i].tag.name < encoderFields[j].tag.name
+ })
+
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ if key != "" {
+ key = key + "."
+ }
+
+ for _, ef := range encoderFields {
+ field := value.FieldByIndex(ef.idx)
+ err := ef.fn(key+ef.tag.name, field, writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ if extraEncoder != nil {
+ err := e.encodeMapEntries(key, value.FieldByIndex(extraEncoder.idx), writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ }
+}
+
+func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
+ f, _ := t.FieldByName("Value")
+ enc := e.typeEncoder(f.Type)
+
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ present := value.FieldByName("Present")
+ if !present.Bool() {
+ return nil
+ }
+ null := value.FieldByName("Null")
+ if null.Bool() {
+ return nil
+ }
+ raw := value.FieldByName("Raw")
+ if !raw.IsNil() {
+ return e.typeEncoder(raw.Type())(key, raw, writer)
+ }
+ return enc(key, value.FieldByName("Value"), writer)
+ }
+}
+
+func (e *encoder) newTimeTypeEncoder() encoderFunc {
+ format := e.dateFormat
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ return writer.WriteField(key, value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format))
+ }
+}
+
+func (e encoder) newInterfaceEncoder() encoderFunc {
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ value = value.Elem()
+ if !value.IsValid() {
+ return nil
+ }
+ return e.typeEncoder(value.Type())(key, value, writer)
+ }
+}
+
+var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
+
+func escapeQuotes(s string) string {
+ return quoteEscaper.Replace(s)
+}
+
+func (e *encoder) newReaderTypeEncoder() encoderFunc {
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ reader := value.Convert(reflect.TypeOf((*io.Reader)(nil)).Elem()).Interface().(io.Reader)
+ filename := "anonymous_file"
+ contentType := "application/octet-stream"
+ if named, ok := reader.(interface{ Filename() string }); ok {
+ filename = named.Filename()
+ } else if named, ok := reader.(interface{ Name() string }); ok {
+ filename = path.Base(named.Name())
+ }
+ if typed, ok := reader.(interface{ ContentType() string }); ok {
+ contentType = typed.ContentType()
+ }
+
+ // Below is taken almost 1-for-1 from [multipart.CreateFormFile]
+ h := make(textproto.MIMEHeader)
+ h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(filename)))
+ h.Set("Content-Type", contentType)
+ filewriter, err := writer.CreatePart(h)
+ if err != nil {
+ return err
+ }
+ _, err = io.Copy(filewriter, reader)
+ return err
+ }
+}
+
+// Given a []byte of json (may either be an empty object or an object that already contains entries)
+// encode all of the entries in the map to the json byte array.
+func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error {
+ type mapPair struct {
+ key string
+ value reflect.Value
+ }
+
+ if key != "" {
+ key = key + "."
+ }
+
+ pairs := []mapPair{}
+
+ iter := v.MapRange()
+ for iter.Next() {
+ if iter.Key().Type().Kind() == reflect.String {
+ pairs = append(pairs, mapPair{key: iter.Key().String(), value: iter.Value()})
+ } else {
+ return fmt.Errorf("cannot encode a map with a non string key")
+ }
+ }
+
+ // Ensure deterministic output
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].key < pairs[j].key
+ })
+
+ elementEncoder := e.typeEncoder(v.Type().Elem())
+ for _, p := range pairs {
+ err := elementEncoder(key+string(p.key), p.value, writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
+ return func(key string, value reflect.Value, writer *multipart.Writer) error {
+ return e.encodeMapEntries(key, value, writer)
+ }
+}
diff --git a/packages/tui/sdk/internal/apiform/form.go b/packages/tui/sdk/internal/apiform/form.go
new file mode 100644
index 000000000..5445116e9
--- /dev/null
+++ b/packages/tui/sdk/internal/apiform/form.go
@@ -0,0 +1,5 @@
+package apiform
+
+type Marshaler interface {
+ MarshalMultipart() ([]byte, string, error)
+}
diff --git a/packages/tui/sdk/internal/apiform/form_test.go b/packages/tui/sdk/internal/apiform/form_test.go
new file mode 100644
index 000000000..39d1460c0
--- /dev/null
+++ b/packages/tui/sdk/internal/apiform/form_test.go
@@ -0,0 +1,440 @@
+package apiform
+
+import (
+ "bytes"
+ "mime/multipart"
+ "strings"
+ "testing"
+ "time"
+)
+
+func P[T any](v T) *T { return &v }
+
+type Primitives struct {
+ A bool `form:"a"`
+ B int `form:"b"`
+ C uint `form:"c"`
+ D float64 `form:"d"`
+ E float32 `form:"e"`
+ F []int `form:"f"`
+}
+
+type PrimitivePointers struct {
+ A *bool `form:"a"`
+ B *int `form:"b"`
+ C *uint `form:"c"`
+ D *float64 `form:"d"`
+ E *float32 `form:"e"`
+ F *[]int `form:"f"`
+}
+
+type Slices struct {
+ Slice []Primitives `form:"slices"`
+}
+
+type DateTime struct {
+ Date time.Time `form:"date" format:"date"`
+ DateTime time.Time `form:"date-time" format:"date-time"`
+}
+
+type AdditionalProperties struct {
+ A bool `form:"a"`
+ Extras map[string]interface{} `form:"-,extras"`
+}
+
+type TypedAdditionalProperties struct {
+ A bool `form:"a"`
+ Extras map[string]int `form:"-,extras"`
+}
+
+type EmbeddedStructs struct {
+ AdditionalProperties
+ A *int `form:"number2"`
+ Extras map[string]interface{} `form:"-,extras"`
+}
+
+type Recursive struct {
+ Name string `form:"name"`
+ Child *Recursive `form:"child"`
+}
+
+type UnknownStruct struct {
+ Unknown interface{} `form:"unknown"`
+}
+
+type UnionStruct struct {
+ Union Union `form:"union" format:"date"`
+}
+
+type Union interface {
+ union()
+}
+
+type UnionInteger int64
+
+func (UnionInteger) union() {}
+
+type UnionStructA struct {
+ Type string `form:"type"`
+ A string `form:"a"`
+ B string `form:"b"`
+}
+
+func (UnionStructA) union() {}
+
+type UnionStructB struct {
+ Type string `form:"type"`
+ A string `form:"a"`
+}
+
+func (UnionStructB) union() {}
+
+type UnionTime time.Time
+
+func (UnionTime) union() {}
+
+type ReaderStruct struct {
+}
+
+var tests = map[string]struct {
+ buf string
+ val interface{}
+}{
+ "map_string": {
+ `--xxx
+Content-Disposition: form-data; name="foo"
+
+bar
+--xxx--
+`,
+ map[string]string{"foo": "bar"},
+ },
+
+ "map_interface": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+1
+--xxx
+Content-Disposition: form-data; name="b"
+
+str
+--xxx
+Content-Disposition: form-data; name="c"
+
+false
+--xxx--
+`,
+ map[string]interface{}{"a": float64(1), "b": "str", "c": false},
+ },
+
+ "primitive_struct": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+false
+--xxx
+Content-Disposition: form-data; name="b"
+
+237628372683
+--xxx
+Content-Disposition: form-data; name="c"
+
+654
+--xxx
+Content-Disposition: form-data; name="d"
+
+9999.43
+--xxx
+Content-Disposition: form-data; name="e"
+
+43.76
+--xxx
+Content-Disposition: form-data; name="f.0"
+
+1
+--xxx
+Content-Disposition: form-data; name="f.1"
+
+2
+--xxx
+Content-Disposition: form-data; name="f.2"
+
+3
+--xxx
+Content-Disposition: form-data; name="f.3"
+
+4
+--xxx--
+`,
+ Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ },
+
+ "slices": {
+ `--xxx
+Content-Disposition: form-data; name="slices.0.a"
+
+false
+--xxx
+Content-Disposition: form-data; name="slices.0.b"
+
+237628372683
+--xxx
+Content-Disposition: form-data; name="slices.0.c"
+
+654
+--xxx
+Content-Disposition: form-data; name="slices.0.d"
+
+9999.43
+--xxx
+Content-Disposition: form-data; name="slices.0.e"
+
+43.76
+--xxx
+Content-Disposition: form-data; name="slices.0.f.0"
+
+1
+--xxx
+Content-Disposition: form-data; name="slices.0.f.1"
+
+2
+--xxx
+Content-Disposition: form-data; name="slices.0.f.2"
+
+3
+--xxx
+Content-Disposition: form-data; name="slices.0.f.3"
+
+4
+--xxx--
+`,
+ Slices{
+ Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
+ },
+ },
+
+ "primitive_pointer_struct": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+false
+--xxx
+Content-Disposition: form-data; name="b"
+
+237628372683
+--xxx
+Content-Disposition: form-data; name="c"
+
+654
+--xxx
+Content-Disposition: form-data; name="d"
+
+9999.43
+--xxx
+Content-Disposition: form-data; name="e"
+
+43.76
+--xxx
+Content-Disposition: form-data; name="f.0"
+
+1
+--xxx
+Content-Disposition: form-data; name="f.1"
+
+2
+--xxx
+Content-Disposition: form-data; name="f.2"
+
+3
+--xxx
+Content-Disposition: form-data; name="f.3"
+
+4
+--xxx
+Content-Disposition: form-data; name="f.4"
+
+5
+--xxx--
+`,
+ PrimitivePointers{
+ A: P(false),
+ B: P(237628372683),
+ C: P(uint(654)),
+ D: P(9999.43),
+ E: P(float32(43.76)),
+ F: &[]int{1, 2, 3, 4, 5},
+ },
+ },
+
+ "datetime_struct": {
+ `--xxx
+Content-Disposition: form-data; name="date"
+
+2006-01-02
+--xxx
+Content-Disposition: form-data; name="date-time"
+
+2006-01-02T15:04:05Z
+--xxx--
+`,
+ DateTime{
+ Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
+ DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
+ },
+ },
+
+ "additional_properties": {
+ `--xxx
+Content-Disposition: form-data; name="a"
+
+true
+--xxx
+Content-Disposition: form-data; name="bar"
+
+value
+--xxx
+Content-Disposition: form-data; name="foo"
+
+true
+--xxx--
+`,
+ AdditionalProperties{
+ A: true,
+ Extras: map[string]interface{}{
+ "bar": "value",
+ "foo": true,
+ },
+ },
+ },
+
+ "recursive_struct": {
+ `--xxx
+Content-Disposition: form-data; name="child.name"
+
+Alex
+--xxx
+Content-Disposition: form-data; name="name"
+
+Robert
+--xxx--
+`,
+ Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
+ },
+
+ "unknown_struct_number": {
+ `--xxx
+Content-Disposition: form-data; name="unknown"
+
+12
+--xxx--
+`,
+ UnknownStruct{
+ Unknown: 12.,
+ },
+ },
+
+ "unknown_struct_map": {
+ `--xxx
+Content-Disposition: form-data; name="unknown.foo"
+
+bar
+--xxx--
+`,
+ UnknownStruct{
+ Unknown: map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+
+ "union_integer": {
+ `--xxx
+Content-Disposition: form-data; name="union"
+
+12
+--xxx--
+`,
+ UnionStruct{
+ Union: UnionInteger(12),
+ },
+ },
+
+ "union_struct_discriminated_a": {
+ `--xxx
+Content-Disposition: form-data; name="union.a"
+
+foo
+--xxx
+Content-Disposition: form-data; name="union.b"
+
+bar
+--xxx
+Content-Disposition: form-data; name="union.type"
+
+typeA
+--xxx--
+`,
+
+ UnionStruct{
+ Union: UnionStructA{
+ Type: "typeA",
+ A: "foo",
+ B: "bar",
+ },
+ },
+ },
+
+ "union_struct_discriminated_b": {
+ `--xxx
+Content-Disposition: form-data; name="union.a"
+
+foo
+--xxx
+Content-Disposition: form-data; name="union.type"
+
+typeB
+--xxx--
+`,
+ UnionStruct{
+ Union: UnionStructB{
+ Type: "typeB",
+ A: "foo",
+ },
+ },
+ },
+
+ "union_struct_time": {
+ `--xxx
+Content-Disposition: form-data; name="union"
+
+2010-05-23
+--xxx--
+`,
+ UnionStruct{
+ Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
+ },
+ },
+}
+
+func TestEncode(t *testing.T) {
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ buf := bytes.NewBuffer(nil)
+ writer := multipart.NewWriter(buf)
+ writer.SetBoundary("xxx")
+ err := Marshal(test.val, writer)
+ if err != nil {
+ t.Errorf("serialization of %v failed with error %v", test.val, err)
+ }
+ err = writer.Close()
+ if err != nil {
+ t.Errorf("serialization of %v failed with error %v", test.val, err)
+ }
+ raw := buf.Bytes()
+ if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") {
+ t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw))
+ }
+ })
+ }
+}
diff --git a/packages/tui/sdk/internal/apiform/tag.go b/packages/tui/sdk/internal/apiform/tag.go
new file mode 100644
index 000000000..b22e054fc
--- /dev/null
+++ b/packages/tui/sdk/internal/apiform/tag.go
@@ -0,0 +1,48 @@
+package apiform
+
+import (
+ "reflect"
+ "strings"
+)
+
+const jsonStructTag = "json"
+const formStructTag = "form"
+const formatStructTag = "format"
+
+type parsedStructTag struct {
+ name string
+ required bool
+ extras bool
+ metadata bool
+}
+
+func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
+ raw, ok := field.Tag.Lookup(formStructTag)
+ if !ok {
+ raw, ok = field.Tag.Lookup(jsonStructTag)
+ }
+ if !ok {
+ return
+ }
+ parts := strings.Split(raw, ",")
+ if len(parts) == 0 {
+ return tag, false
+ }
+ tag.name = parts[0]
+ for _, part := range parts[1:] {
+ switch part {
+ case "required":
+ tag.required = true
+ case "extras":
+ tag.extras = true
+ case "metadata":
+ tag.metadata = true
+ }
+ }
+ return
+}
+
+func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
+ format, ok = field.Tag.Lookup(formatStructTag)
+ return
+}
diff --git a/packages/tui/sdk/internal/apijson/decoder.go b/packages/tui/sdk/internal/apijson/decoder.go
new file mode 100644
index 000000000..68b7ed6be
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/decoder.go
@@ -0,0 +1,670 @@
+package apijson
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+ "strconv"
+ "sync"
+ "time"
+ "unsafe"
+
+ "github.com/tidwall/gjson"
+)
+
+// decoders is a synchronized map with roughly the following type:
+// map[reflect.Type]decoderFunc
+var decoders sync.Map
+
+// Unmarshal is similar to [encoding/json.Unmarshal] and parses the JSON-encoded
+// data and stores it in the given pointer.
+func Unmarshal(raw []byte, to any) error {
+ d := &decoderBuilder{dateFormat: time.RFC3339}
+ return d.unmarshal(raw, to)
+}
+
+// UnmarshalRoot is like Unmarshal, but doesn't try to call MarshalJSON on the
+// root element. Useful if a struct's UnmarshalJSON is overrode to use the
+// behavior of this encoder versus the standard library.
+func UnmarshalRoot(raw []byte, to any) error {
+ d := &decoderBuilder{dateFormat: time.RFC3339, root: true}
+ return d.unmarshal(raw, to)
+}
+
+// decoderBuilder contains the 'compile-time' state of the decoder.
+type decoderBuilder struct {
+ // Whether or not this is the first element and called by [UnmarshalRoot], see
+ // the documentation there to see why this is necessary.
+ root bool
+ // The dateFormat (a format string for [time.Format]) which is chosen by the
+ // last struct tag that was seen.
+ dateFormat string
+}
+
+// decoderState contains the 'run-time' state of the decoder.
+type decoderState struct {
+ strict bool
+ exactness exactness
+}
+
+// Exactness refers to how close to the type the result was if deserialization
+// was successful. This is useful in deserializing unions, where you want to try
+// each entry, first with strict, then with looser validation, without actually
+// having to do a lot of redundant work by marshalling twice (or maybe even more
+// times).
+type exactness int8
+
+const (
+ // Some values had to fudged a bit, for example by converting a string to an
+ // int, or an enum with extra values.
+ loose exactness = iota
+ // There are some extra arguments, but other wise it matches the union.
+ extras
+ // Exactly right.
+ exact
+)
+
+type decoderFunc func(node gjson.Result, value reflect.Value, state *decoderState) error
+
+type decoderField struct {
+ tag parsedStructTag
+ fn decoderFunc
+ idx []int
+ goname string
+}
+
+type decoderEntry struct {
+ reflect.Type
+ dateFormat string
+ root bool
+}
+
+func (d *decoderBuilder) unmarshal(raw []byte, to any) error {
+ value := reflect.ValueOf(to).Elem()
+ result := gjson.ParseBytes(raw)
+ if !value.IsValid() {
+ return fmt.Errorf("apijson: cannot marshal into invalid value")
+ }
+ return d.typeDecoder(value.Type())(result, value, &decoderState{strict: false, exactness: exact})
+}
+
+func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc {
+ entry := decoderEntry{
+ Type: t,
+ dateFormat: d.dateFormat,
+ root: d.root,
+ }
+
+ if fi, ok := decoders.Load(entry); ok {
+ return fi.(decoderFunc)
+ }
+
+ // To deal with recursive types, populate the map with an
+ // indirect func before we build it. This type waits on the
+ // real func (f) to be ready and then calls it. This indirect
+ // func is only used for recursive types.
+ var (
+ wg sync.WaitGroup
+ f decoderFunc
+ )
+ wg.Add(1)
+ fi, loaded := decoders.LoadOrStore(entry, decoderFunc(func(node gjson.Result, v reflect.Value, state *decoderState) error {
+ wg.Wait()
+ return f(node, v, state)
+ }))
+ if loaded {
+ return fi.(decoderFunc)
+ }
+
+ // Compute the real decoder and replace the indirect func with it.
+ f = d.newTypeDecoder(t)
+ wg.Done()
+ decoders.Store(entry, f)
+ return f
+}
+
+func indirectUnmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error {
+ return v.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw))
+}
+
+func unmarshalerDecoder(n gjson.Result, v reflect.Value, state *decoderState) error {
+ if v.Kind() == reflect.Pointer && v.CanSet() {
+ v.Set(reflect.New(v.Type().Elem()))
+ }
+ return v.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(n.Raw))
+}
+
+func (d *decoderBuilder) newTypeDecoder(t reflect.Type) decoderFunc {
+ if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
+ return d.newTimeTypeDecoder(t)
+ }
+ if !d.root && t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) {
+ return unmarshalerDecoder
+ }
+ if !d.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) {
+ if _, ok := unionVariants[t]; !ok {
+ return indirectUnmarshalerDecoder
+ }
+ }
+ d.root = false
+
+ if _, ok := unionRegistry[t]; ok {
+ return d.newUnionDecoder(t)
+ }
+
+ switch t.Kind() {
+ case reflect.Pointer:
+ inner := t.Elem()
+ innerDecoder := d.typeDecoder(inner)
+
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ if !v.IsValid() {
+ return fmt.Errorf("apijson: unexpected invalid reflection value %+#v", v)
+ }
+
+ newValue := reflect.New(inner).Elem()
+ err := innerDecoder(n, newValue, state)
+ if err != nil {
+ return err
+ }
+
+ v.Set(newValue.Addr())
+ return nil
+ }
+ case reflect.Struct:
+ return d.newStructTypeDecoder(t)
+ case reflect.Array:
+ fallthrough
+ case reflect.Slice:
+ return d.newArrayTypeDecoder(t)
+ case reflect.Map:
+ return d.newMapDecoder(t)
+ case reflect.Interface:
+ return func(node gjson.Result, value reflect.Value, state *decoderState) error {
+ if !value.IsValid() {
+ return fmt.Errorf("apijson: unexpected invalid value %+#v", value)
+ }
+ if node.Value() != nil && value.CanSet() {
+ value.Set(reflect.ValueOf(node.Value()))
+ }
+ return nil
+ }
+ default:
+ return d.newPrimitiveTypeDecoder(t)
+ }
+}
+
+// newUnionDecoder returns a decoderFunc that deserializes into a union using an
+// algorithm roughly similar to Pydantic's [smart algorithm].
+//
+// Conceptually this is equivalent to choosing the best schema based on how 'exact'
+// the deserialization is for each of the schemas.
+//
+// If there is a tie in the level of exactness, then the tie is broken
+// left-to-right.
+//
+// [smart algorithm]: https://docs.pydantic.dev/latest/concepts/unions/#smart-mode
+func (d *decoderBuilder) newUnionDecoder(t reflect.Type) decoderFunc {
+ unionEntry, ok := unionRegistry[t]
+ if !ok {
+ panic("apijson: couldn't find union of type " + t.String() + " in union registry")
+ }
+ decoders := []decoderFunc{}
+ for _, variant := range unionEntry.variants {
+ decoder := d.typeDecoder(variant.Type)
+ decoders = append(decoders, decoder)
+ }
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ // If there is a discriminator match, circumvent the exactness logic entirely
+ for idx, variant := range unionEntry.variants {
+ decoder := decoders[idx]
+ if variant.TypeFilter != n.Type {
+ continue
+ }
+
+ if len(unionEntry.discriminatorKey) != 0 {
+ discriminatorValue := n.Get(unionEntry.discriminatorKey).Value()
+ if discriminatorValue == variant.DiscriminatorValue {
+ inner := reflect.New(variant.Type).Elem()
+ err := decoder(n, inner, state)
+ v.Set(inner)
+ return err
+ }
+ }
+ }
+
+ // Set bestExactness to worse than loose
+ bestExactness := loose - 1
+ for idx, variant := range unionEntry.variants {
+ decoder := decoders[idx]
+ if variant.TypeFilter != n.Type {
+ continue
+ }
+ sub := decoderState{strict: state.strict, exactness: exact}
+ inner := reflect.New(variant.Type).Elem()
+ err := decoder(n, inner, &sub)
+ if err != nil {
+ continue
+ }
+ if sub.exactness == exact {
+ v.Set(inner)
+ return nil
+ }
+ if sub.exactness > bestExactness {
+ v.Set(inner)
+ bestExactness = sub.exactness
+ }
+ }
+
+ if bestExactness < loose {
+ return errors.New("apijson: was not able to coerce type as union")
+ }
+
+ if guardStrict(state, bestExactness != exact) {
+ return errors.New("apijson: was not able to coerce type as union strictly")
+ }
+
+ return nil
+ }
+}
+
+func (d *decoderBuilder) newMapDecoder(t reflect.Type) decoderFunc {
+ keyType := t.Key()
+ itemType := t.Elem()
+ itemDecoder := d.typeDecoder(itemType)
+
+ return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
+ mapValue := reflect.MakeMapWithSize(t, len(node.Map()))
+
+ node.ForEach(func(key, value gjson.Result) bool {
+ // It's fine for us to just use `ValueOf` here because the key types will
+ // always be primitive types so we don't need to decode it using the standard pattern
+ keyValue := reflect.ValueOf(key.Value())
+ if !keyValue.IsValid() {
+ if err == nil {
+ err = fmt.Errorf("apijson: received invalid key type %v", keyValue.String())
+ }
+ return false
+ }
+ if keyValue.Type() != keyType {
+ if err == nil {
+ err = fmt.Errorf("apijson: expected key type %v but got %v", keyType, keyValue.Type())
+ }
+ return false
+ }
+
+ itemValue := reflect.New(itemType).Elem()
+ itemerr := itemDecoder(value, itemValue, state)
+ if itemerr != nil {
+ if err == nil {
+ err = itemerr
+ }
+ return false
+ }
+
+ mapValue.SetMapIndex(keyValue, itemValue)
+ return true
+ })
+
+ if err != nil {
+ return err
+ }
+ value.Set(mapValue)
+ return nil
+ }
+}
+
+func (d *decoderBuilder) newArrayTypeDecoder(t reflect.Type) decoderFunc {
+ itemDecoder := d.typeDecoder(t.Elem())
+
+ return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
+ if !node.IsArray() {
+ return fmt.Errorf("apijson: could not deserialize to an array")
+ }
+
+ arrayNode := node.Array()
+
+ arrayValue := reflect.MakeSlice(reflect.SliceOf(t.Elem()), len(arrayNode), len(arrayNode))
+ for i, itemNode := range arrayNode {
+ err = itemDecoder(itemNode, arrayValue.Index(i), state)
+ if err != nil {
+ return err
+ }
+ }
+
+ value.Set(arrayValue)
+ return nil
+ }
+}
+
+func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc {
+ // map of json field name to struct field decoders
+ decoderFields := map[string]decoderField{}
+ anonymousDecoders := []decoderField{}
+ extraDecoder := (*decoderField)(nil)
+ inlineDecoder := (*decoderField)(nil)
+
+ for i := 0; i < t.NumField(); i++ {
+ idx := []int{i}
+ field := t.FieldByIndex(idx)
+ if !field.IsExported() {
+ continue
+ }
+ // If this is an embedded struct, traverse one level deeper to extract
+ // the fields and get their encoders as well.
+ if field.Anonymous {
+ anonymousDecoders = append(anonymousDecoders, decoderField{
+ fn: d.typeDecoder(field.Type),
+ idx: idx[:],
+ })
+ continue
+ }
+ // If json tag is not present, then we skip, which is intentionally
+ // different behavior from the stdlib.
+ ptag, ok := parseJSONStructTag(field)
+ if !ok {
+ continue
+ }
+ // We only want to support unexported fields if they're tagged with
+ // `extras` because that field shouldn't be part of the public API.
+ if ptag.extras {
+ extraDecoder = &decoderField{ptag, d.typeDecoder(field.Type.Elem()), idx, field.Name}
+ continue
+ }
+ if ptag.inline {
+ inlineDecoder = &decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name}
+ continue
+ }
+ if ptag.metadata {
+ continue
+ }
+
+ oldFormat := d.dateFormat
+ dateFormat, ok := parseFormatStructTag(field)
+ if ok {
+ switch dateFormat {
+ case "date-time":
+ d.dateFormat = time.RFC3339
+ case "date":
+ d.dateFormat = "2006-01-02"
+ }
+ }
+ decoderFields[ptag.name] = decoderField{ptag, d.typeDecoder(field.Type), idx, field.Name}
+ d.dateFormat = oldFormat
+ }
+
+ return func(node gjson.Result, value reflect.Value, state *decoderState) (err error) {
+ if field := value.FieldByName("JSON"); field.IsValid() {
+ if raw := field.FieldByName("raw"); raw.IsValid() {
+ setUnexportedField(raw, node.Raw)
+ }
+ }
+
+ for _, decoder := range anonymousDecoders {
+ // ignore errors
+ decoder.fn(node, value.FieldByIndex(decoder.idx), state)
+ }
+
+ if inlineDecoder != nil {
+ var meta Field
+ dest := value.FieldByIndex(inlineDecoder.idx)
+ isValid := false
+ if dest.IsValid() && node.Type != gjson.Null {
+ err = inlineDecoder.fn(node, dest, state)
+ if err == nil {
+ isValid = true
+ }
+ }
+
+ if node.Type == gjson.Null {
+ meta = Field{
+ raw: node.Raw,
+ status: null,
+ }
+ } else if !isValid {
+ meta = Field{
+ raw: node.Raw,
+ status: invalid,
+ }
+ } else if isValid {
+ meta = Field{
+ raw: node.Raw,
+ status: valid,
+ }
+ }
+ if metadata := getSubField(value, inlineDecoder.idx, inlineDecoder.goname); metadata.IsValid() {
+ metadata.Set(reflect.ValueOf(meta))
+ }
+ return err
+ }
+
+ typedExtraType := reflect.Type(nil)
+ typedExtraFields := reflect.Value{}
+ if extraDecoder != nil {
+ typedExtraType = value.FieldByIndex(extraDecoder.idx).Type()
+ typedExtraFields = reflect.MakeMap(typedExtraType)
+ }
+ untypedExtraFields := map[string]Field{}
+
+ for fieldName, itemNode := range node.Map() {
+ df, explicit := decoderFields[fieldName]
+ var (
+ dest reflect.Value
+ fn decoderFunc
+ meta Field
+ )
+ if explicit {
+ fn = df.fn
+ dest = value.FieldByIndex(df.idx)
+ }
+ if !explicit && extraDecoder != nil {
+ dest = reflect.New(typedExtraType.Elem()).Elem()
+ fn = extraDecoder.fn
+ }
+
+ isValid := false
+ if dest.IsValid() && itemNode.Type != gjson.Null {
+ err = fn(itemNode, dest, state)
+ if err == nil {
+ isValid = true
+ }
+ }
+
+ if itemNode.Type == gjson.Null {
+ meta = Field{
+ raw: itemNode.Raw,
+ status: null,
+ }
+ } else if !isValid {
+ meta = Field{
+ raw: itemNode.Raw,
+ status: invalid,
+ }
+ } else if isValid {
+ meta = Field{
+ raw: itemNode.Raw,
+ status: valid,
+ }
+ }
+
+ if explicit {
+ if metadata := getSubField(value, df.idx, df.goname); metadata.IsValid() {
+ metadata.Set(reflect.ValueOf(meta))
+ }
+ }
+ if !explicit {
+ untypedExtraFields[fieldName] = meta
+ }
+ if !explicit && extraDecoder != nil {
+ typedExtraFields.SetMapIndex(reflect.ValueOf(fieldName), dest)
+ }
+ }
+
+ if extraDecoder != nil && typedExtraFields.Len() > 0 {
+ value.FieldByIndex(extraDecoder.idx).Set(typedExtraFields)
+ }
+
+ // Set exactness to 'extras' if there are untyped, extra fields.
+ if len(untypedExtraFields) > 0 && state.exactness > extras {
+ state.exactness = extras
+ }
+
+ if metadata := getSubField(value, []int{-1}, "ExtraFields"); metadata.IsValid() && len(untypedExtraFields) > 0 {
+ metadata.Set(reflect.ValueOf(untypedExtraFields))
+ }
+ return nil
+ }
+}
+
+func (d *decoderBuilder) newPrimitiveTypeDecoder(t reflect.Type) decoderFunc {
+ switch t.Kind() {
+ case reflect.String:
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ v.SetString(n.String())
+ if guardStrict(state, n.Type != gjson.String) {
+ return fmt.Errorf("apijson: failed to parse string strictly")
+ }
+ // Everything that is not an object can be loosely stringified.
+ if n.Type == gjson.JSON {
+ return fmt.Errorf("apijson: failed to parse string")
+ }
+ if guardUnknown(state, v) {
+ return fmt.Errorf("apijson: failed string enum validation")
+ }
+ return nil
+ }
+ case reflect.Bool:
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ v.SetBool(n.Bool())
+ if guardStrict(state, n.Type != gjson.True && n.Type != gjson.False) {
+ return fmt.Errorf("apijson: failed to parse bool strictly")
+ }
+ // Numbers and strings that are either 'true' or 'false' can be loosely
+ // deserialized as bool.
+ if n.Type == gjson.String && (n.Raw != "true" && n.Raw != "false") || n.Type == gjson.JSON {
+ return fmt.Errorf("apijson: failed to parse bool")
+ }
+ if guardUnknown(state, v) {
+ return fmt.Errorf("apijson: failed bool enum validation")
+ }
+ return nil
+ }
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ v.SetInt(n.Int())
+ if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num))) {
+ return fmt.Errorf("apijson: failed to parse int strictly")
+ }
+ // Numbers, booleans, and strings that maybe look like numbers can be
+ // loosely deserialized as numbers.
+ if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
+ return fmt.Errorf("apijson: failed to parse int")
+ }
+ if guardUnknown(state, v) {
+ return fmt.Errorf("apijson: failed int enum validation")
+ }
+ return nil
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ v.SetUint(n.Uint())
+ if guardStrict(state, n.Type != gjson.Number || n.Num != float64(int(n.Num)) || n.Num < 0) {
+ return fmt.Errorf("apijson: failed to parse uint strictly")
+ }
+ // Numbers, booleans, and strings that maybe look like numbers can be
+ // loosely deserialized as uint.
+ if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
+ return fmt.Errorf("apijson: failed to parse uint")
+ }
+ if guardUnknown(state, v) {
+ return fmt.Errorf("apijson: failed uint enum validation")
+ }
+ return nil
+ }
+ case reflect.Float32, reflect.Float64:
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ v.SetFloat(n.Float())
+ if guardStrict(state, n.Type != gjson.Number) {
+ return fmt.Errorf("apijson: failed to parse float strictly")
+ }
+ // Numbers, booleans, and strings that maybe look like numbers can be
+ // loosely deserialized as floats.
+ if n.Type == gjson.JSON || (n.Type == gjson.String && !canParseAsNumber(n.Str)) {
+ return fmt.Errorf("apijson: failed to parse float")
+ }
+ if guardUnknown(state, v) {
+ return fmt.Errorf("apijson: failed float enum validation")
+ }
+ return nil
+ }
+ default:
+ return func(node gjson.Result, v reflect.Value, state *decoderState) error {
+ return fmt.Errorf("unknown type received at primitive decoder: %s", t.String())
+ }
+ }
+}
+
+func (d *decoderBuilder) newTimeTypeDecoder(t reflect.Type) decoderFunc {
+ format := d.dateFormat
+ return func(n gjson.Result, v reflect.Value, state *decoderState) error {
+ parsed, err := time.Parse(format, n.Str)
+ if err == nil {
+ v.Set(reflect.ValueOf(parsed).Convert(t))
+ return nil
+ }
+
+ if guardStrict(state, true) {
+ return err
+ }
+
+ layouts := []string{
+ "2006-01-02",
+ "2006-01-02T15:04:05Z07:00",
+ "2006-01-02T15:04:05Z0700",
+ "2006-01-02T15:04:05",
+ "2006-01-02 15:04:05Z07:00",
+ "2006-01-02 15:04:05Z0700",
+ "2006-01-02 15:04:05",
+ }
+
+ for _, layout := range layouts {
+ parsed, err := time.Parse(layout, n.Str)
+ if err == nil {
+ v.Set(reflect.ValueOf(parsed).Convert(t))
+ return nil
+ }
+ }
+
+ return fmt.Errorf("unable to leniently parse date-time string: %s", n.Str)
+ }
+}
+
+func setUnexportedField(field reflect.Value, value interface{}) {
+ reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value))
+}
+
+func guardStrict(state *decoderState, cond bool) bool {
+ if !cond {
+ return false
+ }
+
+ if state.strict {
+ return true
+ }
+
+ state.exactness = loose
+ return false
+}
+
+func canParseAsNumber(str string) bool {
+ _, err := strconv.ParseFloat(str, 64)
+ return err == nil
+}
+
+func guardUnknown(state *decoderState, v reflect.Value) bool {
+ if have, ok := v.Interface().(interface{ IsKnown() bool }); guardStrict(state, ok && !have.IsKnown()) {
+ return true
+ }
+ return false
+}
diff --git a/packages/tui/sdk/internal/apijson/encoder.go b/packages/tui/sdk/internal/apijson/encoder.go
new file mode 100644
index 000000000..0e5f89e17
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/encoder.go
@@ -0,0 +1,398 @@
+package apijson
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/tidwall/sjson"
+
+ "github.com/sst/opencode-sdk-go/internal/param"
+)
+
+var encoders sync.Map // map[encoderEntry]encoderFunc
+
+func Marshal(value interface{}) ([]byte, error) {
+ e := &encoder{dateFormat: time.RFC3339}
+ return e.marshal(value)
+}
+
+func MarshalRoot(value interface{}) ([]byte, error) {
+ e := &encoder{root: true, dateFormat: time.RFC3339}
+ return e.marshal(value)
+}
+
+type encoder struct {
+ dateFormat string
+ root bool
+}
+
+type encoderFunc func(value reflect.Value) ([]byte, error)
+
+type encoderField struct {
+ tag parsedStructTag
+ fn encoderFunc
+ idx []int
+}
+
+type encoderEntry struct {
+ reflect.Type
+ dateFormat string
+ root bool
+}
+
+func (e *encoder) marshal(value interface{}) ([]byte, error) {
+ val := reflect.ValueOf(value)
+ if !val.IsValid() {
+ return nil, nil
+ }
+ typ := val.Type()
+ enc := e.typeEncoder(typ)
+ return enc(val)
+}
+
+func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
+ entry := encoderEntry{
+ Type: t,
+ dateFormat: e.dateFormat,
+ root: e.root,
+ }
+
+ if fi, ok := encoders.Load(entry); ok {
+ return fi.(encoderFunc)
+ }
+
+ // To deal with recursive types, populate the map with an
+ // indirect func before we build it. This type waits on the
+ // real func (f) to be ready and then calls it. This indirect
+ // func is only used for recursive types.
+ var (
+ wg sync.WaitGroup
+ f encoderFunc
+ )
+ wg.Add(1)
+ fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(v reflect.Value) ([]byte, error) {
+ wg.Wait()
+ return f(v)
+ }))
+ if loaded {
+ return fi.(encoderFunc)
+ }
+
+ // Compute the real encoder and replace the indirect func with it.
+ f = e.newTypeEncoder(t)
+ wg.Done()
+ encoders.Store(entry, f)
+ return f
+}
+
+func marshalerEncoder(v reflect.Value) ([]byte, error) {
+ return v.Interface().(json.Marshaler).MarshalJSON()
+}
+
+func indirectMarshalerEncoder(v reflect.Value) ([]byte, error) {
+ return v.Addr().Interface().(json.Marshaler).MarshalJSON()
+}
+
+func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
+ if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
+ return e.newTimeTypeEncoder()
+ }
+ if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
+ return marshalerEncoder
+ }
+ if !e.root && reflect.PointerTo(t).Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
+ return indirectMarshalerEncoder
+ }
+ e.root = false
+ switch t.Kind() {
+ case reflect.Pointer:
+ inner := t.Elem()
+
+ innerEncoder := e.typeEncoder(inner)
+ return func(v reflect.Value) ([]byte, error) {
+ if !v.IsValid() || v.IsNil() {
+ return nil, nil
+ }
+ return innerEncoder(v.Elem())
+ }
+ case reflect.Struct:
+ return e.newStructTypeEncoder(t)
+ case reflect.Array:
+ fallthrough
+ case reflect.Slice:
+ return e.newArrayTypeEncoder(t)
+ case reflect.Map:
+ return e.newMapEncoder(t)
+ case reflect.Interface:
+ return e.newInterfaceEncoder()
+ default:
+ return e.newPrimitiveTypeEncoder(t)
+ }
+}
+
+func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
+ switch t.Kind() {
+ // Note that we could use `gjson` to encode these types but it would complicate our
+ // code more and this current code shouldn't cause any issues
+ case reflect.String:
+ return func(v reflect.Value) ([]byte, error) {
+ return json.Marshal(v.Interface())
+ }
+ case reflect.Bool:
+ return func(v reflect.Value) ([]byte, error) {
+ if v.Bool() {
+ return []byte("true"), nil
+ }
+ return []byte("false"), nil
+ }
+ case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
+ return func(v reflect.Value) ([]byte, error) {
+ return []byte(strconv.FormatInt(v.Int(), 10)), nil
+ }
+ case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return func(v reflect.Value) ([]byte, error) {
+ return []byte(strconv.FormatUint(v.Uint(), 10)), nil
+ }
+ case reflect.Float32:
+ return func(v reflect.Value) ([]byte, error) {
+ return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 32)), nil
+ }
+ case reflect.Float64:
+ return func(v reflect.Value) ([]byte, error) {
+ return []byte(strconv.FormatFloat(v.Float(), 'f', -1, 64)), nil
+ }
+ default:
+ return func(v reflect.Value) ([]byte, error) {
+ return nil, fmt.Errorf("unknown type received at primitive encoder: %s", t.String())
+ }
+ }
+}
+
+func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
+ itemEncoder := e.typeEncoder(t.Elem())
+
+ return func(value reflect.Value) ([]byte, error) {
+ json := []byte("[]")
+ for i := 0; i < value.Len(); i++ {
+ var value, err = itemEncoder(value.Index(i))
+ if err != nil {
+ return nil, err
+ }
+ if value == nil {
+ // Assume that empty items should be inserted as `null` so that the output array
+ // will be the same length as the input array
+ value = []byte("null")
+ }
+
+ json, err = sjson.SetRawBytes(json, "-1", value)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return json, nil
+ }
+}
+
+func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
+ if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
+ return e.newFieldTypeEncoder(t)
+ }
+
+ encoderFields := []encoderField{}
+ extraEncoder := (*encoderField)(nil)
+
+ // This helper allows us to recursively collect field encoders into a flat
+ // array. The parameter `index` keeps track of the access patterns necessary
+ // to get to some field.
+ var collectEncoderFields func(r reflect.Type, index []int)
+ collectEncoderFields = func(r reflect.Type, index []int) {
+ for i := 0; i < r.NumField(); i++ {
+ idx := append(index, i)
+ field := t.FieldByIndex(idx)
+ if !field.IsExported() {
+ continue
+ }
+ // If this is an embedded struct, traverse one level deeper to extract
+ // the field and get their encoders as well.
+ if field.Anonymous {
+ collectEncoderFields(field.Type, idx)
+ continue
+ }
+ // If json tag is not present, then we skip, which is intentionally
+ // different behavior from the stdlib.
+ ptag, ok := parseJSONStructTag(field)
+ if !ok {
+ continue
+ }
+ // We only want to support unexported field if they're tagged with
+ // `extras` because that field shouldn't be part of the public API. We
+ // also want to only keep the top level extras
+ if ptag.extras && len(index) == 0 {
+ extraEncoder = &encoderField{ptag, e.typeEncoder(field.Type.Elem()), idx}
+ continue
+ }
+ if ptag.name == "-" {
+ continue
+ }
+
+ dateFormat, ok := parseFormatStructTag(field)
+ oldFormat := e.dateFormat
+ if ok {
+ switch dateFormat {
+ case "date-time":
+ e.dateFormat = time.RFC3339
+ case "date":
+ e.dateFormat = "2006-01-02"
+ }
+ }
+ encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
+ e.dateFormat = oldFormat
+ }
+ }
+ collectEncoderFields(t, []int{})
+
+ // Ensure deterministic output by sorting by lexicographic order
+ sort.Slice(encoderFields, func(i, j int) bool {
+ return encoderFields[i].tag.name < encoderFields[j].tag.name
+ })
+
+ return func(value reflect.Value) (json []byte, err error) {
+ json = []byte("{}")
+
+ for _, ef := range encoderFields {
+ field := value.FieldByIndex(ef.idx)
+ encoded, err := ef.fn(field)
+ if err != nil {
+ return nil, err
+ }
+ if encoded == nil {
+ continue
+ }
+ json, err = sjson.SetRawBytes(json, ef.tag.name, encoded)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if extraEncoder != nil {
+ json, err = e.encodeMapEntries(json, value.FieldByIndex(extraEncoder.idx))
+ if err != nil {
+ return nil, err
+ }
+ }
+ return
+ }
+}
+
+func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
+ f, _ := t.FieldByName("Value")
+ enc := e.typeEncoder(f.Type)
+
+ return func(value reflect.Value) (json []byte, err error) {
+ present := value.FieldByName("Present")
+ if !present.Bool() {
+ return nil, nil
+ }
+ null := value.FieldByName("Null")
+ if null.Bool() {
+ return []byte("null"), nil
+ }
+ raw := value.FieldByName("Raw")
+ if !raw.IsNil() {
+ return e.typeEncoder(raw.Type())(raw)
+ }
+ return enc(value.FieldByName("Value"))
+ }
+}
+
+func (e *encoder) newTimeTypeEncoder() encoderFunc {
+ format := e.dateFormat
+ return func(value reflect.Value) (json []byte, err error) {
+ return []byte(`"` + value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format) + `"`), nil
+ }
+}
+
+func (e encoder) newInterfaceEncoder() encoderFunc {
+ return func(value reflect.Value) ([]byte, error) {
+ value = value.Elem()
+ if !value.IsValid() {
+ return nil, nil
+ }
+ return e.typeEncoder(value.Type())(value)
+ }
+}
+
+// Given a []byte of json (may either be an empty object or an object that already contains entries)
+// encode all of the entries in the map to the json byte array.
+func (e *encoder) encodeMapEntries(json []byte, v reflect.Value) ([]byte, error) {
+ type mapPair struct {
+ key []byte
+ value reflect.Value
+ }
+
+ pairs := []mapPair{}
+ keyEncoder := e.typeEncoder(v.Type().Key())
+
+ iter := v.MapRange()
+ for iter.Next() {
+ var encodedKeyString string
+ if iter.Key().Type().Kind() == reflect.String {
+ encodedKeyString = iter.Key().String()
+ } else {
+ var err error
+ encodedKeyBytes, err := keyEncoder(iter.Key())
+ if err != nil {
+ return nil, err
+ }
+ encodedKeyString = string(encodedKeyBytes)
+ }
+ encodedKey := []byte(sjsonReplacer.Replace(encodedKeyString))
+ pairs = append(pairs, mapPair{key: encodedKey, value: iter.Value()})
+ }
+
+ // Ensure deterministic output
+ sort.Slice(pairs, func(i, j int) bool {
+ return bytes.Compare(pairs[i].key, pairs[j].key) < 0
+ })
+
+ elementEncoder := e.typeEncoder(v.Type().Elem())
+ for _, p := range pairs {
+ encodedValue, err := elementEncoder(p.value)
+ if err != nil {
+ return nil, err
+ }
+ if len(encodedValue) == 0 {
+ continue
+ }
+ json, err = sjson.SetRawBytes(json, string(p.key), encodedValue)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return json, nil
+}
+
+func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
+ return func(value reflect.Value) ([]byte, error) {
+ json := []byte("{}")
+ var err error
+ json, err = e.encodeMapEntries(json, value)
+ if err != nil {
+ return nil, err
+ }
+ return json, nil
+ }
+}
+
+// If we want to set a literal key value into JSON using sjson, we need to make sure it doesn't have
+// special characters that sjson interprets as a path.
+var sjsonReplacer *strings.Replacer = strings.NewReplacer(".", "\\.", ":", "\\:", "*", "\\*")
diff --git a/packages/tui/sdk/internal/apijson/field.go b/packages/tui/sdk/internal/apijson/field.go
new file mode 100644
index 000000000..3ef207c58
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/field.go
@@ -0,0 +1,41 @@
+package apijson
+
+import "reflect"
+
+type status uint8
+
+const (
+ missing status = iota
+ null
+ invalid
+ valid
+)
+
+type Field struct {
+ raw string
+ status status
+}
+
+// Returns true if the field is explicitly `null` _or_ if it is not present at all (ie, missing).
+// To check if the field's key is present in the JSON with an explicit null value,
+// you must check `f.IsNull() && !f.IsMissing()`.
+func (j Field) IsNull() bool { return j.status <= null }
+func (j Field) IsMissing() bool { return j.status == missing }
+func (j Field) IsInvalid() bool { return j.status == invalid }
+func (j Field) Raw() string { return j.raw }
+
+func getSubField(root reflect.Value, index []int, name string) reflect.Value {
+ strct := root.FieldByIndex(index[:len(index)-1])
+ if !strct.IsValid() {
+ panic("couldn't find encapsulating struct for field " + name)
+ }
+ meta := strct.FieldByName("JSON")
+ if !meta.IsValid() {
+ return reflect.Value{}
+ }
+ field := meta.FieldByName(name)
+ if !field.IsValid() {
+ return reflect.Value{}
+ }
+ return field
+}
diff --git a/packages/tui/sdk/internal/apijson/field_test.go b/packages/tui/sdk/internal/apijson/field_test.go
new file mode 100644
index 000000000..2e170c76f
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/field_test.go
@@ -0,0 +1,66 @@
+package apijson
+
+import (
+ "testing"
+ "time"
+
+ "github.com/sst/opencode-sdk-go/internal/param"
+)
+
+type Struct struct {
+ A string `json:"a"`
+ B int64 `json:"b"`
+}
+
+type FieldStruct struct {
+ A param.Field[string] `json:"a"`
+ B param.Field[int64] `json:"b"`
+ C param.Field[Struct] `json:"c"`
+ D param.Field[time.Time] `json:"d" format:"date"`
+ E param.Field[time.Time] `json:"e" format:"date-time"`
+ F param.Field[int64] `json:"f"`
+}
+
+func TestFieldMarshal(t *testing.T) {
+ tests := map[string]struct {
+ value interface{}
+ expected string
+ }{
+ "null_string": {param.Field[string]{Present: true, Null: true}, "null"},
+ "null_int": {param.Field[int]{Present: true, Null: true}, "null"},
+ "null_int64": {param.Field[int64]{Present: true, Null: true}, "null"},
+ "null_struct": {param.Field[Struct]{Present: true, Null: true}, "null"},
+
+ "string": {param.Field[string]{Present: true, Value: "string"}, `"string"`},
+ "int": {param.Field[int]{Present: true, Value: 123}, "123"},
+ "int64": {param.Field[int64]{Present: true, Value: int64(123456789123456789)}, "123456789123456789"},
+ "struct": {param.Field[Struct]{Present: true, Value: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`},
+
+ "string_raw": {param.Field[int]{Present: true, Raw: "string"}, `"string"`},
+ "int_raw": {param.Field[int]{Present: true, Raw: 123}, "123"},
+ "int64_raw": {param.Field[int]{Present: true, Raw: int64(123456789123456789)}, "123456789123456789"},
+ "struct_raw": {param.Field[int]{Present: true, Raw: Struct{A: "yo", B: 123}}, `{"a":"yo","b":123}`},
+
+ "param_struct": {
+ FieldStruct{
+ A: param.Field[string]{Present: true, Value: "hello"},
+ B: param.Field[int64]{Present: true, Value: int64(12)},
+ D: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)},
+ E: param.Field[time.Time]{Present: true, Value: time.Date(2023, time.March, 18, 14, 47, 38, 0, time.UTC)},
+ },
+ `{"a":"hello","b":12,"d":"2023-03-18","e":"2023-03-18T14:47:38Z"}`,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ b, err := Marshal(test.value)
+ if err != nil {
+ t.Fatalf("didn't expect error %v", err)
+ }
+ if string(b) != test.expected {
+ t.Fatalf("expected %s, received %s", test.expected, string(b))
+ }
+ })
+ }
+}
diff --git a/packages/tui/sdk/internal/apijson/json_test.go b/packages/tui/sdk/internal/apijson/json_test.go
new file mode 100644
index 000000000..e6563448f
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/json_test.go
@@ -0,0 +1,617 @@
+package apijson
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/tidwall/gjson"
+)
+
+func P[T any](v T) *T { return &v }
+
+type Primitives struct {
+ A bool `json:"a"`
+ B int `json:"b"`
+ C uint `json:"c"`
+ D float64 `json:"d"`
+ E float32 `json:"e"`
+ F []int `json:"f"`
+}
+
+type PrimitivePointers struct {
+ A *bool `json:"a"`
+ B *int `json:"b"`
+ C *uint `json:"c"`
+ D *float64 `json:"d"`
+ E *float32 `json:"e"`
+ F *[]int `json:"f"`
+}
+
+type Slices struct {
+ Slice []Primitives `json:"slices"`
+}
+
+type DateTime struct {
+ Date time.Time `json:"date" format:"date"`
+ DateTime time.Time `json:"date-time" format:"date-time"`
+}
+
+type AdditionalProperties struct {
+ A bool `json:"a"`
+ ExtraFields map[string]interface{} `json:"-,extras"`
+}
+
+type TypedAdditionalProperties struct {
+ A bool `json:"a"`
+ ExtraFields map[string]int `json:"-,extras"`
+}
+
+type EmbeddedStruct struct {
+ A bool `json:"a"`
+ B string `json:"b"`
+
+ JSON EmbeddedStructJSON
+}
+
+type EmbeddedStructJSON struct {
+ A Field
+ B Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+type EmbeddedStructs struct {
+ EmbeddedStruct
+ A *int `json:"a"`
+ ExtraFields map[string]interface{} `json:"-,extras"`
+
+ JSON EmbeddedStructsJSON
+}
+
+type EmbeddedStructsJSON struct {
+ A Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+type Recursive struct {
+ Name string `json:"name"`
+ Child *Recursive `json:"child"`
+}
+
+type JSONFieldStruct struct {
+ A bool `json:"a"`
+ B int64 `json:"b"`
+ C string `json:"c"`
+ D string `json:"d"`
+ ExtraFields map[string]int64 `json:"-,extras"`
+ JSON JSONFieldStructJSON `json:"-,metadata"`
+}
+
+type JSONFieldStructJSON struct {
+ A Field
+ B Field
+ C Field
+ D Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+type UnknownStruct struct {
+ Unknown interface{} `json:"unknown"`
+}
+
+type UnionStruct struct {
+ Union Union `json:"union" format:"date"`
+}
+
+type Union interface {
+ union()
+}
+
+type Inline struct {
+ InlineField Primitives `json:"-,inline"`
+ JSON InlineJSON `json:"-,metadata"`
+}
+
+type InlineArray struct {
+ InlineField []string `json:"-,inline"`
+ JSON InlineJSON `json:"-,metadata"`
+}
+
+type InlineJSON struct {
+ InlineField Field
+ raw string
+}
+
+type UnionInteger int64
+
+func (UnionInteger) union() {}
+
+type UnionStructA struct {
+ Type string `json:"type"`
+ A string `json:"a"`
+ B string `json:"b"`
+}
+
+func (UnionStructA) union() {}
+
+type UnionStructB struct {
+ Type string `json:"type"`
+ A string `json:"a"`
+}
+
+func (UnionStructB) union() {}
+
+type UnionTime time.Time
+
+func (UnionTime) union() {}
+
+func init() {
+ RegisterUnion(reflect.TypeOf((*Union)(nil)).Elem(), "type",
+ UnionVariant{
+ TypeFilter: gjson.String,
+ Type: reflect.TypeOf(UnionTime{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.Number,
+ Type: reflect.TypeOf(UnionInteger(0)),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ DiscriminatorValue: "typeA",
+ Type: reflect.TypeOf(UnionStructA{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ DiscriminatorValue: "typeB",
+ Type: reflect.TypeOf(UnionStructB{}),
+ },
+ )
+}
+
+type ComplexUnionStruct struct {
+ Union ComplexUnion `json:"union"`
+}
+
+type ComplexUnion interface {
+ complexUnion()
+}
+
+type ComplexUnionA struct {
+ Boo string `json:"boo"`
+ Foo bool `json:"foo"`
+}
+
+func (ComplexUnionA) complexUnion() {}
+
+type ComplexUnionB struct {
+ Boo bool `json:"boo"`
+ Foo string `json:"foo"`
+}
+
+func (ComplexUnionB) complexUnion() {}
+
+type ComplexUnionC struct {
+ Boo int64 `json:"boo"`
+}
+
+func (ComplexUnionC) complexUnion() {}
+
+type ComplexUnionTypeA struct {
+ Baz int64 `json:"baz"`
+ Type TypeA `json:"type"`
+}
+
+func (ComplexUnionTypeA) complexUnion() {}
+
+type TypeA string
+
+func (t TypeA) IsKnown() bool {
+ return t == "a"
+}
+
+type ComplexUnionTypeB struct {
+ Baz int64 `json:"baz"`
+ Type TypeB `json:"type"`
+}
+
+type TypeB string
+
+func (t TypeB) IsKnown() bool {
+ return t == "b"
+}
+
+type UnmarshalStruct struct {
+ Foo string `json:"foo"`
+ prop bool `json:"-"`
+}
+
+func (r *UnmarshalStruct) UnmarshalJSON(json []byte) error {
+ r.prop = true
+ return UnmarshalRoot(json, r)
+}
+
+func (ComplexUnionTypeB) complexUnion() {}
+
+func init() {
+ RegisterUnion(reflect.TypeOf((*ComplexUnion)(nil)).Elem(), "",
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ComplexUnionA{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ComplexUnionB{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ComplexUnionC{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ComplexUnionTypeA{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ComplexUnionTypeB{}),
+ },
+ )
+}
+
+type MarshallingUnionStruct struct {
+ Union MarshallingUnion
+}
+
+func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) {
+ *r = MarshallingUnionStruct{}
+ err = UnmarshalRoot(data, &r.Union)
+ return
+}
+
+func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) {
+ return MarshalRoot(r.Union)
+}
+
+type MarshallingUnion interface {
+ marshallingUnion()
+}
+
+type MarshallingUnionA struct {
+ Boo string `json:"boo"`
+}
+
+func (MarshallingUnionA) marshallingUnion() {}
+
+func (r *MarshallingUnionA) UnmarshalJSON(data []byte) (err error) {
+ return UnmarshalRoot(data, r)
+}
+
+type MarshallingUnionB struct {
+ Foo string `json:"foo"`
+}
+
+func (MarshallingUnionB) marshallingUnion() {}
+
+func (r *MarshallingUnionB) UnmarshalJSON(data []byte) (err error) {
+ return UnmarshalRoot(data, r)
+}
+
+func init() {
+ RegisterUnion(
+ reflect.TypeOf((*MarshallingUnion)(nil)).Elem(),
+ "",
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(MarshallingUnionA{}),
+ },
+ UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(MarshallingUnionB{}),
+ },
+ )
+}
+
+var tests = map[string]struct {
+ buf string
+ val interface{}
+}{
+ "true": {"true", true},
+ "false": {"false", false},
+ "int": {"1", 1},
+ "int_bigger": {"12324", 12324},
+ "int_string_coerce": {`"65"`, 65},
+ "int_boolean_coerce": {"true", 1},
+ "int64": {"1", int64(1)},
+ "int64_huge": {"123456789123456789", int64(123456789123456789)},
+ "uint": {"1", uint(1)},
+ "uint_bigger": {"12324", uint(12324)},
+ "uint_coerce": {`"65"`, uint(65)},
+ "float_1.54": {"1.54", float32(1.54)},
+ "float_1.89": {"1.89", float64(1.89)},
+ "string": {`"str"`, "str"},
+ "string_int_coerce": {`12`, "12"},
+ "array_string": {`["foo","bar"]`, []string{"foo", "bar"}},
+ "array_int": {`[1,2]`, []int{1, 2}},
+ "array_int_coerce": {`["1",2]`, []int{1, 2}},
+
+ "ptr_true": {"true", P(true)},
+ "ptr_false": {"false", P(false)},
+ "ptr_int": {"1", P(1)},
+ "ptr_int_bigger": {"12324", P(12324)},
+ "ptr_int_string_coerce": {`"65"`, P(65)},
+ "ptr_int_boolean_coerce": {"true", P(1)},
+ "ptr_int64": {"1", P(int64(1))},
+ "ptr_int64_huge": {"123456789123456789", P(int64(123456789123456789))},
+ "ptr_uint": {"1", P(uint(1))},
+ "ptr_uint_bigger": {"12324", P(uint(12324))},
+ "ptr_uint_coerce": {`"65"`, P(uint(65))},
+ "ptr_float_1.54": {"1.54", P(float32(1.54))},
+ "ptr_float_1.89": {"1.89", P(float64(1.89))},
+
+ "date_time": {`"2007-03-01T13:00:00Z"`, time.Date(2007, time.March, 1, 13, 0, 0, 0, time.UTC)},
+ "date_time_nano_coerce": {`"2007-03-01T13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)},
+
+ "date_time_missing_t_coerce": {`"2007-03-01 13:03:05Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)},
+ "date_time_missing_timezone_coerce": {`"2007-03-01T13:03:05"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.UTC)},
+ // note: using -1200 to minimize probability of conflicting with the local timezone of the test runner
+ // see https://en.wikipedia.org/wiki/UTC%E2%88%9212:00
+ "date_time_missing_timezone_colon_coerce": {`"2007-03-01T13:03:05-1200"`, time.Date(2007, time.March, 1, 13, 3, 5, 0, time.FixedZone("", -12*60*60))},
+ "date_time_nano_missing_t_coerce": {`"2007-03-01 13:03:05.123456789Z"`, time.Date(2007, time.March, 1, 13, 3, 5, 123456789, time.UTC)},
+
+ "map_string": {`{"foo":"bar"}`, map[string]string{"foo": "bar"}},
+ "map_string_with_sjson_path_chars": {`{":a.b.c*:d*-1e.f":"bar"}`, map[string]string{":a.b.c*:d*-1e.f": "bar"}},
+ "map_interface": {`{"a":1,"b":"str","c":false}`, map[string]interface{}{"a": float64(1), "b": "str", "c": false}},
+
+ "primitive_struct": {
+ `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`,
+ Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ },
+
+ "slices": {
+ `{"slices":[{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}]}`,
+ Slices{
+ Slice: []Primitives{{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}}},
+ },
+ },
+
+ "primitive_pointer_struct": {
+ `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4,5]}`,
+ PrimitivePointers{
+ A: P(false),
+ B: P(237628372683),
+ C: P(uint(654)),
+ D: P(9999.43),
+ E: P(float32(43.76)),
+ F: &[]int{1, 2, 3, 4, 5},
+ },
+ },
+
+ "datetime_struct": {
+ `{"date":"2006-01-02","date-time":"2006-01-02T15:04:05Z"}`,
+ DateTime{
+ Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
+ DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
+ },
+ },
+
+ "additional_properties": {
+ `{"a":true,"bar":"value","foo":true}`,
+ AdditionalProperties{
+ A: true,
+ ExtraFields: map[string]interface{}{
+ "bar": "value",
+ "foo": true,
+ },
+ },
+ },
+
+ "embedded_struct": {
+ `{"a":1,"b":"bar"}`,
+ EmbeddedStructs{
+ EmbeddedStruct: EmbeddedStruct{
+ A: true,
+ B: "bar",
+ JSON: EmbeddedStructJSON{
+ A: Field{raw: `1`, status: valid},
+ B: Field{raw: `"bar"`, status: valid},
+ raw: `{"a":1,"b":"bar"}`,
+ },
+ },
+ A: P(1),
+ ExtraFields: map[string]interface{}{"b": "bar"},
+ JSON: EmbeddedStructsJSON{
+ A: Field{raw: `1`, status: valid},
+ ExtraFields: map[string]Field{
+ "b": {raw: `"bar"`, status: valid},
+ },
+ raw: `{"a":1,"b":"bar"}`,
+ },
+ },
+ },
+
+ "recursive_struct": {
+ `{"child":{"name":"Alex"},"name":"Robert"}`,
+ Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
+ },
+
+ "metadata_coerce": {
+ `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`,
+ JSONFieldStruct{
+ A: false,
+ B: 12,
+ C: "",
+ JSON: JSONFieldStructJSON{
+ raw: `{"a":"12","b":"12","c":null,"extra_typed":12,"extra_untyped":{"foo":"bar"}}`,
+ A: Field{raw: `"12"`, status: invalid},
+ B: Field{raw: `"12"`, status: valid},
+ C: Field{raw: "null", status: null},
+ D: Field{raw: "", status: missing},
+ ExtraFields: map[string]Field{
+ "extra_typed": {
+ raw: "12",
+ status: valid,
+ },
+ "extra_untyped": {
+ raw: `{"foo":"bar"}`,
+ status: invalid,
+ },
+ },
+ },
+ ExtraFields: map[string]int64{
+ "extra_typed": 12,
+ "extra_untyped": 0,
+ },
+ },
+ },
+
+ "unknown_struct_number": {
+ `{"unknown":12}`,
+ UnknownStruct{
+ Unknown: 12.,
+ },
+ },
+
+ "unknown_struct_map": {
+ `{"unknown":{"foo":"bar"}}`,
+ UnknownStruct{
+ Unknown: map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ },
+
+ "union_integer": {
+ `{"union":12}`,
+ UnionStruct{
+ Union: UnionInteger(12),
+ },
+ },
+
+ "union_struct_discriminated_a": {
+ `{"union":{"a":"foo","b":"bar","type":"typeA"}}`,
+ UnionStruct{
+ Union: UnionStructA{
+ Type: "typeA",
+ A: "foo",
+ B: "bar",
+ },
+ },
+ },
+
+ "union_struct_discriminated_b": {
+ `{"union":{"a":"foo","type":"typeB"}}`,
+ UnionStruct{
+ Union: UnionStructB{
+ Type: "typeB",
+ A: "foo",
+ },
+ },
+ },
+
+ "union_struct_time": {
+ `{"union":"2010-05-23"}`,
+ UnionStruct{
+ Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
+ },
+ },
+
+ "complex_union_a": {
+ `{"union":{"boo":"12","foo":true}}`,
+ ComplexUnionStruct{Union: ComplexUnionA{Boo: "12", Foo: true}},
+ },
+
+ "complex_union_b": {
+ `{"union":{"boo":true,"foo":"12"}}`,
+ ComplexUnionStruct{Union: ComplexUnionB{Boo: true, Foo: "12"}},
+ },
+
+ "complex_union_c": {
+ `{"union":{"boo":12}}`,
+ ComplexUnionStruct{Union: ComplexUnionC{Boo: 12}},
+ },
+
+ "complex_union_type_a": {
+ `{"union":{"baz":12,"type":"a"}}`,
+ ComplexUnionStruct{Union: ComplexUnionTypeA{Baz: 12, Type: TypeA("a")}},
+ },
+
+ "complex_union_type_b": {
+ `{"union":{"baz":12,"type":"b"}}`,
+ ComplexUnionStruct{Union: ComplexUnionTypeB{Baz: 12, Type: TypeB("b")}},
+ },
+
+ "marshalling_union_a": {
+ `{"boo":"hello"}`,
+ MarshallingUnionStruct{Union: MarshallingUnionA{Boo: "hello"}},
+ },
+ "marshalling_union_b": {
+ `{"foo":"hi"}`,
+ MarshallingUnionStruct{Union: MarshallingUnionB{Foo: "hi"}},
+ },
+
+ "unmarshal": {
+ `{"foo":"hello"}`,
+ &UnmarshalStruct{Foo: "hello", prop: true},
+ },
+
+ "array_of_unmarshal": {
+ `[{"foo":"hello"}]`,
+ []UnmarshalStruct{{Foo: "hello", prop: true}},
+ },
+
+ "inline_coerce": {
+ `{"a":false,"b":237628372683,"c":654,"d":9999.43,"e":43.76,"f":[1,2,3,4]}`,
+ Inline{
+ InlineField: Primitives{A: false, B: 237628372683, C: 0x28e, D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ JSON: InlineJSON{
+ InlineField: Field{raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}", status: 3},
+ raw: "{\"a\":false,\"b\":237628372683,\"c\":654,\"d\":9999.43,\"e\":43.76,\"f\":[1,2,3,4]}",
+ },
+ },
+ },
+
+ "inline_array_coerce": {
+ `["Hello","foo","bar"]`,
+ InlineArray{
+ InlineField: []string{"Hello", "foo", "bar"},
+ JSON: InlineJSON{
+ InlineField: Field{raw: `["Hello","foo","bar"]`, status: 3},
+ raw: `["Hello","foo","bar"]`,
+ },
+ },
+ },
+}
+
+func TestDecode(t *testing.T) {
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ result := reflect.New(reflect.TypeOf(test.val))
+ if err := Unmarshal([]byte(test.buf), result.Interface()); err != nil {
+ t.Fatalf("deserialization of %v failed with error %v", result, err)
+ }
+ if !reflect.DeepEqual(result.Elem().Interface(), test.val) {
+ t.Fatalf("expected '%s' to deserialize to \n%#v\nbut got\n%#v", test.buf, test.val, result.Elem().Interface())
+ }
+ })
+ }
+}
+
+func TestEncode(t *testing.T) {
+ for name, test := range tests {
+ if strings.HasSuffix(name, "_coerce") {
+ continue
+ }
+ t.Run(name, func(t *testing.T) {
+ raw, err := Marshal(test.val)
+ if err != nil {
+ t.Fatalf("serialization of %v failed with error %v", test.val, err)
+ }
+ if string(raw) != test.buf {
+ t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.buf, string(raw))
+ }
+ })
+ }
+}
diff --git a/packages/tui/sdk/internal/apijson/port.go b/packages/tui/sdk/internal/apijson/port.go
new file mode 100644
index 000000000..502ab7780
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/port.go
@@ -0,0 +1,120 @@
+package apijson
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Port copies over values from one struct to another struct.
+func Port(from any, to any) error {
+ toVal := reflect.ValueOf(to)
+ fromVal := reflect.ValueOf(from)
+
+ if toVal.Kind() != reflect.Ptr || toVal.IsNil() {
+ return fmt.Errorf("destination must be a non-nil pointer")
+ }
+
+ for toVal.Kind() == reflect.Ptr {
+ toVal = toVal.Elem()
+ }
+ toType := toVal.Type()
+
+ for fromVal.Kind() == reflect.Ptr {
+ fromVal = fromVal.Elem()
+ }
+ fromType := fromVal.Type()
+
+ if toType.Kind() != reflect.Struct {
+ return fmt.Errorf("destination must be a non-nil pointer to a struct (%v %v)", toType, toType.Kind())
+ }
+
+ values := map[string]reflect.Value{}
+ fields := map[string]reflect.Value{}
+
+ fromJSON := fromVal.FieldByName("JSON")
+ toJSON := toVal.FieldByName("JSON")
+
+ // Iterate through the fields of v and load all the "normal" fields in the struct to the map of
+ // string to reflect.Value, as well as their raw .JSON.Foo counterpart indicated by j.
+ var getFields func(t reflect.Type, v reflect.Value)
+ getFields = func(t reflect.Type, v reflect.Value) {
+ j := v.FieldByName("JSON")
+
+ // Recurse into anonymous fields first, since the fields on the object should win over the fields in the
+ // embedded object.
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+ if field.Anonymous {
+ getFields(field.Type, v.Field(i))
+ continue
+ }
+ }
+
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+ ptag, ok := parseJSONStructTag(field)
+ if !ok || ptag.name == "-" {
+ continue
+ }
+ values[ptag.name] = v.Field(i)
+ if j.IsValid() {
+ fields[ptag.name] = j.FieldByName(field.Name)
+ }
+ }
+ }
+ getFields(fromType, fromVal)
+
+ // Use the values from the previous step to populate the 'to' struct.
+ for i := 0; i < toType.NumField(); i++ {
+ field := toType.Field(i)
+ ptag, ok := parseJSONStructTag(field)
+ if !ok {
+ continue
+ }
+ if ptag.name == "-" {
+ continue
+ }
+ if value, ok := values[ptag.name]; ok {
+ delete(values, ptag.name)
+ if field.Type.Kind() == reflect.Interface {
+ toVal.Field(i).Set(value)
+ } else {
+ switch value.Kind() {
+ case reflect.String:
+ toVal.Field(i).SetString(value.String())
+ case reflect.Bool:
+ toVal.Field(i).SetBool(value.Bool())
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ toVal.Field(i).SetInt(value.Int())
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ toVal.Field(i).SetUint(value.Uint())
+ case reflect.Float32, reflect.Float64:
+ toVal.Field(i).SetFloat(value.Float())
+ default:
+ toVal.Field(i).Set(value)
+ }
+ }
+ }
+
+ if fromJSONField, ok := fields[ptag.name]; ok {
+ if toJSONField := toJSON.FieldByName(field.Name); toJSONField.IsValid() {
+ toJSONField.Set(fromJSONField)
+ }
+ }
+ }
+
+ // Finally, copy over the .JSON.raw and .JSON.ExtraFields
+ if toJSON.IsValid() {
+ if raw := toJSON.FieldByName("raw"); raw.IsValid() {
+ setUnexportedField(raw, fromJSON.Interface().(interface{ RawJSON() string }).RawJSON())
+ }
+
+ if toExtraFields := toJSON.FieldByName("ExtraFields"); toExtraFields.IsValid() {
+ if fromExtraFields := fromJSON.FieldByName("ExtraFields"); fromExtraFields.IsValid() {
+ setUnexportedField(toExtraFields, fromExtraFields.Interface())
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/packages/tui/sdk/internal/apijson/port_test.go b/packages/tui/sdk/internal/apijson/port_test.go
new file mode 100644
index 000000000..115405335
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/port_test.go
@@ -0,0 +1,257 @@
+package apijson
+
+import (
+ "reflect"
+ "testing"
+)
+
+type Metadata struct {
+ CreatedAt string `json:"created_at"`
+}
+
+// Card is the "combined" type of CardVisa and CardMastercard
+type Card struct {
+ Processor CardProcessor `json:"processor"`
+ Data any `json:"data"`
+ IsFoo bool `json:"is_foo"`
+ IsBar bool `json:"is_bar"`
+ Metadata Metadata `json:"metadata"`
+ Value interface{} `json:"value"`
+
+ JSON cardJSON
+}
+
+type cardJSON struct {
+ Processor Field
+ Data Field
+ IsFoo Field
+ IsBar Field
+ Metadata Field
+ Value Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+func (r cardJSON) RawJSON() string { return r.raw }
+
+type CardProcessor string
+
+// CardVisa
+type CardVisa struct {
+ Processor CardVisaProcessor `json:"processor"`
+ Data CardVisaData `json:"data"`
+ IsFoo bool `json:"is_foo"`
+ Metadata Metadata `json:"metadata"`
+ Value string `json:"value"`
+
+ JSON cardVisaJSON
+}
+
+type cardVisaJSON struct {
+ Processor Field
+ Data Field
+ IsFoo Field
+ Metadata Field
+ Value Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+func (r cardVisaJSON) RawJSON() string { return r.raw }
+
+type CardVisaProcessor string
+
+type CardVisaData struct {
+ Foo string `json:"foo"`
+}
+
+// CardMastercard
+type CardMastercard struct {
+ Processor CardMastercardProcessor `json:"processor"`
+ Data CardMastercardData `json:"data"`
+ IsBar bool `json:"is_bar"`
+ Metadata Metadata `json:"metadata"`
+ Value bool `json:"value"`
+
+ JSON cardMastercardJSON
+}
+
+type cardMastercardJSON struct {
+ Processor Field
+ Data Field
+ IsBar Field
+ Metadata Field
+ Value Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+func (r cardMastercardJSON) RawJSON() string { return r.raw }
+
+type CardMastercardProcessor string
+
+type CardMastercardData struct {
+ Bar int64 `json:"bar"`
+}
+
+type CommonFields struct {
+ Metadata Metadata `json:"metadata"`
+ Value string `json:"value"`
+
+ JSON commonFieldsJSON
+}
+
+type commonFieldsJSON struct {
+ Metadata Field
+ Value Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+type CardEmbedded struct {
+ CommonFields
+ Processor CardVisaProcessor `json:"processor"`
+ Data CardVisaData `json:"data"`
+ IsFoo bool `json:"is_foo"`
+
+ JSON cardEmbeddedJSON
+}
+
+type cardEmbeddedJSON struct {
+ Processor Field
+ Data Field
+ IsFoo Field
+ ExtraFields map[string]Field
+ raw string
+}
+
+func (r cardEmbeddedJSON) RawJSON() string { return r.raw }
+
+var portTests = map[string]struct {
+ from any
+ to any
+}{
+ "visa to card": {
+ CardVisa{
+ Processor: "visa",
+ IsFoo: true,
+ Data: CardVisaData{
+ Foo: "foo",
+ },
+ Metadata: Metadata{
+ CreatedAt: "Mar 29 2024",
+ },
+ Value: "value",
+ JSON: cardVisaJSON{
+ raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`,
+ Processor: Field{raw: `"visa"`, status: valid},
+ IsFoo: Field{raw: `true`, status: valid},
+ Data: Field{raw: `{"foo":"foo"}`, status: valid},
+ Value: Field{raw: `"value"`, status: valid},
+ ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}},
+ },
+ },
+ Card{
+ Processor: "visa",
+ IsFoo: true,
+ IsBar: false,
+ Data: CardVisaData{
+ Foo: "foo",
+ },
+ Metadata: Metadata{
+ CreatedAt: "Mar 29 2024",
+ },
+ Value: "value",
+ JSON: cardJSON{
+ raw: `{"processor":"visa","is_foo":true,"data":{"foo":"foo"}}`,
+ Processor: Field{raw: `"visa"`, status: valid},
+ IsFoo: Field{raw: `true`, status: valid},
+ Data: Field{raw: `{"foo":"foo"}`, status: valid},
+ Value: Field{raw: `"value"`, status: valid},
+ ExtraFields: map[string]Field{"extra": {raw: `"yo"`, status: valid}},
+ },
+ },
+ },
+ "mastercard to card": {
+ CardMastercard{
+ Processor: "mastercard",
+ IsBar: true,
+ Data: CardMastercardData{
+ Bar: 13,
+ },
+ Value: false,
+ },
+ Card{
+ Processor: "mastercard",
+ IsFoo: false,
+ IsBar: true,
+ Data: CardMastercardData{
+ Bar: 13,
+ },
+ Value: false,
+ },
+ },
+ "embedded to card": {
+ CardEmbedded{
+ CommonFields: CommonFields{
+ Metadata: Metadata{
+ CreatedAt: "Mar 29 2024",
+ },
+ Value: "embedded_value",
+ JSON: commonFieldsJSON{
+ Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: valid},
+ Value: Field{raw: `"embedded_value"`, status: valid},
+ raw: `should not matter`,
+ },
+ },
+ Processor: "visa",
+ IsFoo: true,
+ Data: CardVisaData{
+ Foo: "embedded_foo",
+ },
+ JSON: cardEmbeddedJSON{
+ raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`,
+ Processor: Field{raw: `"visa"`, status: valid},
+ IsFoo: Field{raw: `true`, status: valid},
+ Data: Field{raw: `{"foo":"embedded_foo"}`, status: valid},
+ },
+ },
+ Card{
+ Processor: "visa",
+ IsFoo: true,
+ IsBar: false,
+ Data: CardVisaData{
+ Foo: "embedded_foo",
+ },
+ Metadata: Metadata{
+ CreatedAt: "Mar 29 2024",
+ },
+ Value: "embedded_value",
+ JSON: cardJSON{
+ raw: `{"processor":"visa","is_foo":true,"data":{"foo":"embedded_foo"},"metadata":{"created_at":"Mar 29 2024"},"value":"embedded_value"}`,
+ Processor: Field{raw: `"visa"`, status: 0x3},
+ IsFoo: Field{raw: "true", status: 0x3},
+ Data: Field{raw: `{"foo":"embedded_foo"}`, status: 0x3},
+ Metadata: Field{raw: `{"created_at":"Mar 29 2024"}`, status: 0x3},
+ Value: Field{raw: `"embedded_value"`, status: 0x3},
+ },
+ },
+ },
+}
+
+func TestPort(t *testing.T) {
+ for name, test := range portTests {
+ t.Run(name, func(t *testing.T) {
+ toVal := reflect.New(reflect.TypeOf(test.to))
+
+ err := Port(test.from, toVal.Interface())
+ if err != nil {
+ t.Fatalf("port of %v failed with error %v", test.from, err)
+ }
+
+ if !reflect.DeepEqual(toVal.Elem().Interface(), test.to) {
+ t.Fatalf("expected:\n%+#v\n\nto port to:\n%+#v\n\nbut got:\n%+#v", test.from, test.to, toVal.Elem().Interface())
+ }
+ })
+ }
+}
diff --git a/packages/tui/sdk/internal/apijson/registry.go b/packages/tui/sdk/internal/apijson/registry.go
new file mode 100644
index 000000000..119cc5ff8
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/registry.go
@@ -0,0 +1,41 @@
+package apijson
+
+import (
+ "reflect"
+
+ "github.com/tidwall/gjson"
+)
+
+type UnionVariant struct {
+ TypeFilter gjson.Type
+ DiscriminatorValue interface{}
+ Type reflect.Type
+}
+
+var unionRegistry = map[reflect.Type]unionEntry{}
+var unionVariants = map[reflect.Type]interface{}{}
+
+type unionEntry struct {
+ discriminatorKey string
+ variants []UnionVariant
+}
+
+func RegisterUnion(typ reflect.Type, discriminator string, variants ...UnionVariant) {
+ unionRegistry[typ] = unionEntry{
+ discriminatorKey: discriminator,
+ variants: variants,
+ }
+ for _, variant := range variants {
+ unionVariants[variant.Type] = typ
+ }
+}
+
+// Useful to wrap a union type to force it to use [apijson.UnmarshalJSON] since you cannot define an
+// UnmarshalJSON function on the interface itself.
+type UnionUnmarshaler[T any] struct {
+ Value T
+}
+
+func (c *UnionUnmarshaler[T]) UnmarshalJSON(buf []byte) error {
+ return UnmarshalRoot(buf, &c.Value)
+}
diff --git a/packages/tui/sdk/internal/apijson/tag.go b/packages/tui/sdk/internal/apijson/tag.go
new file mode 100644
index 000000000..812fb3caf
--- /dev/null
+++ b/packages/tui/sdk/internal/apijson/tag.go
@@ -0,0 +1,47 @@
+package apijson
+
+import (
+ "reflect"
+ "strings"
+)
+
+const jsonStructTag = "json"
+const formatStructTag = "format"
+
+type parsedStructTag struct {
+ name string
+ required bool
+ extras bool
+ metadata bool
+ inline bool
+}
+
+func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
+ raw, ok := field.Tag.Lookup(jsonStructTag)
+ if !ok {
+ return
+ }
+ parts := strings.Split(raw, ",")
+ if len(parts) == 0 {
+ return tag, false
+ }
+ tag.name = parts[0]
+ for _, part := range parts[1:] {
+ switch part {
+ case "required":
+ tag.required = true
+ case "extras":
+ tag.extras = true
+ case "metadata":
+ tag.metadata = true
+ case "inline":
+ tag.inline = true
+ }
+ }
+ return
+}
+
+func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
+ format, ok = field.Tag.Lookup(formatStructTag)
+ return
+}
diff --git a/packages/tui/sdk/internal/apiquery/encoder.go b/packages/tui/sdk/internal/apiquery/encoder.go
new file mode 100644
index 000000000..0922c2316
--- /dev/null
+++ b/packages/tui/sdk/internal/apiquery/encoder.go
@@ -0,0 +1,341 @@
+package apiquery
+
+import (
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/sst/opencode-sdk-go/internal/param"
+)
+
+var encoders sync.Map // map[reflect.Type]encoderFunc
+
+type encoder struct {
+ dateFormat string
+ root bool
+ settings QuerySettings
+}
+
+type encoderFunc func(key string, value reflect.Value) []Pair
+
+type encoderField struct {
+ tag parsedStructTag
+ fn encoderFunc
+ idx []int
+}
+
+type encoderEntry struct {
+ reflect.Type
+ dateFormat string
+ root bool
+ settings QuerySettings
+}
+
+type Pair struct {
+ key string
+ value string
+}
+
+func (e *encoder) typeEncoder(t reflect.Type) encoderFunc {
+ entry := encoderEntry{
+ Type: t,
+ dateFormat: e.dateFormat,
+ root: e.root,
+ settings: e.settings,
+ }
+
+ if fi, ok := encoders.Load(entry); ok {
+ return fi.(encoderFunc)
+ }
+
+ // To deal with recursive types, populate the map with an
+ // indirect func before we build it. This type waits on the
+ // real func (f) to be ready and then calls it. This indirect
+ // func is only used for recursive types.
+ var (
+ wg sync.WaitGroup
+ f encoderFunc
+ )
+ wg.Add(1)
+ fi, loaded := encoders.LoadOrStore(entry, encoderFunc(func(key string, v reflect.Value) []Pair {
+ wg.Wait()
+ return f(key, v)
+ }))
+ if loaded {
+ return fi.(encoderFunc)
+ }
+
+ // Compute the real encoder and replace the indirect func with it.
+ f = e.newTypeEncoder(t)
+ wg.Done()
+ encoders.Store(entry, f)
+ return f
+}
+
+func marshalerEncoder(key string, value reflect.Value) []Pair {
+ s, _ := value.Interface().(json.Marshaler).MarshalJSON()
+ return []Pair{{key, string(s)}}
+}
+
+func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc {
+ if t.ConvertibleTo(reflect.TypeOf(time.Time{})) {
+ return e.newTimeTypeEncoder(t)
+ }
+ if !e.root && t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) {
+ return marshalerEncoder
+ }
+ e.root = false
+ switch t.Kind() {
+ case reflect.Pointer:
+ encoder := e.typeEncoder(t.Elem())
+ return func(key string, value reflect.Value) (pairs []Pair) {
+ if !value.IsValid() || value.IsNil() {
+ return
+ }
+ pairs = encoder(key, value.Elem())
+ return
+ }
+ case reflect.Struct:
+ return e.newStructTypeEncoder(t)
+ case reflect.Array:
+ fallthrough
+ case reflect.Slice:
+ return e.newArrayTypeEncoder(t)
+ case reflect.Map:
+ return e.newMapEncoder(t)
+ case reflect.Interface:
+ return e.newInterfaceEncoder()
+ default:
+ return e.newPrimitiveTypeEncoder(t)
+ }
+}
+
+func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc {
+ if t.Implements(reflect.TypeOf((*param.FieldLike)(nil)).Elem()) {
+ return e.newFieldTypeEncoder(t)
+ }
+
+ encoderFields := []encoderField{}
+
+ // This helper allows us to recursively collect field encoders into a flat
+ // array. The parameter `index` keeps track of the access patterns necessary
+ // to get to some field.
+ var collectEncoderFields func(r reflect.Type, index []int)
+ collectEncoderFields = func(r reflect.Type, index []int) {
+ for i := 0; i < r.NumField(); i++ {
+ idx := append(index, i)
+ field := t.FieldByIndex(idx)
+ if !field.IsExported() {
+ continue
+ }
+ // If this is an embedded struct, traverse one level deeper to extract
+ // the field and get their encoders as well.
+ if field.Anonymous {
+ collectEncoderFields(field.Type, idx)
+ continue
+ }
+ // If query tag is not present, then we skip, which is intentionally
+ // different behavior from the stdlib.
+ ptag, ok := parseQueryStructTag(field)
+ if !ok {
+ continue
+ }
+
+ if ptag.name == "-" && !ptag.inline {
+ continue
+ }
+
+ dateFormat, ok := parseFormatStructTag(field)
+ oldFormat := e.dateFormat
+ if ok {
+ switch dateFormat {
+ case "date-time":
+ e.dateFormat = time.RFC3339
+ case "date":
+ e.dateFormat = "2006-01-02"
+ }
+ }
+ encoderFields = append(encoderFields, encoderField{ptag, e.typeEncoder(field.Type), idx})
+ e.dateFormat = oldFormat
+ }
+ }
+ collectEncoderFields(t, []int{})
+
+ return func(key string, value reflect.Value) (pairs []Pair) {
+ for _, ef := range encoderFields {
+ var subkey string = e.renderKeyPath(key, ef.tag.name)
+ if ef.tag.inline {
+ subkey = key
+ }
+
+ field := value.FieldByIndex(ef.idx)
+ pairs = append(pairs, ef.fn(subkey, field)...)
+ }
+ return
+ }
+}
+
+func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc {
+ keyEncoder := e.typeEncoder(t.Key())
+ elementEncoder := e.typeEncoder(t.Elem())
+ return func(key string, value reflect.Value) (pairs []Pair) {
+ iter := value.MapRange()
+ for iter.Next() {
+ encodedKey := keyEncoder("", iter.Key())
+ if len(encodedKey) != 1 {
+ panic("Unexpected number of parts for encoded map key. Are you using a non-primitive for this map?")
+ }
+ subkey := encodedKey[0].value
+ keyPath := e.renderKeyPath(key, subkey)
+ pairs = append(pairs, elementEncoder(keyPath, iter.Value())...)
+ }
+ return
+ }
+}
+
+func (e *encoder) renderKeyPath(key string, subkey string) string {
+ if len(key) == 0 {
+ return subkey
+ }
+ if e.settings.NestedFormat == NestedQueryFormatDots {
+ return fmt.Sprintf("%s.%s", key, subkey)
+ }
+ return fmt.Sprintf("%s[%s]", key, subkey)
+}
+
+func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc {
+ switch e.settings.ArrayFormat {
+ case ArrayQueryFormatComma:
+ innerEncoder := e.typeEncoder(t.Elem())
+ return func(key string, v reflect.Value) []Pair {
+ elements := []string{}
+ for i := 0; i < v.Len(); i++ {
+ for _, pair := range innerEncoder("", v.Index(i)) {
+ elements = append(elements, pair.value)
+ }
+ }
+ if len(elements) == 0 {
+ return []Pair{}
+ }
+ return []Pair{{key, strings.Join(elements, ",")}}
+ }
+ case ArrayQueryFormatRepeat:
+ innerEncoder := e.typeEncoder(t.Elem())
+ return func(key string, value reflect.Value) (pairs []Pair) {
+ for i := 0; i < value.Len(); i++ {
+ pairs = append(pairs, innerEncoder(key, value.Index(i))...)
+ }
+ return pairs
+ }
+ case ArrayQueryFormatIndices:
+ panic("The array indices format is not supported yet")
+ case ArrayQueryFormatBrackets:
+ innerEncoder := e.typeEncoder(t.Elem())
+ return func(key string, value reflect.Value) []Pair {
+ pairs := []Pair{}
+ for i := 0; i < value.Len(); i++ {
+ pairs = append(pairs, innerEncoder(key+"[]", value.Index(i))...)
+ }
+ return pairs
+ }
+ default:
+ panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat))
+ }
+}
+
+func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc {
+ switch t.Kind() {
+ case reflect.Pointer:
+ inner := t.Elem()
+
+ innerEncoder := e.newPrimitiveTypeEncoder(inner)
+ return func(key string, v reflect.Value) []Pair {
+ if !v.IsValid() || v.IsNil() {
+ return nil
+ }
+ return innerEncoder(key, v.Elem())
+ }
+ case reflect.String:
+ return func(key string, v reflect.Value) []Pair {
+ return []Pair{{key, v.String()}}
+ }
+ case reflect.Bool:
+ return func(key string, v reflect.Value) []Pair {
+ if v.Bool() {
+ return []Pair{{key, "true"}}
+ }
+ return []Pair{{key, "false"}}
+ }
+ case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
+ return func(key string, v reflect.Value) []Pair {
+ return []Pair{{key, strconv.FormatInt(v.Int(), 10)}}
+ }
+ case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ return func(key string, v reflect.Value) []Pair {
+ return []Pair{{key, strconv.FormatUint(v.Uint(), 10)}}
+ }
+ case reflect.Float32, reflect.Float64:
+ return func(key string, v reflect.Value) []Pair {
+ return []Pair{{key, strconv.FormatFloat(v.Float(), 'f', -1, 64)}}
+ }
+ case reflect.Complex64, reflect.Complex128:
+ bitSize := 64
+ if t.Kind() == reflect.Complex128 {
+ bitSize = 128
+ }
+ return func(key string, v reflect.Value) []Pair {
+ return []Pair{{key, strconv.FormatComplex(v.Complex(), 'f', -1, bitSize)}}
+ }
+ default:
+ return func(key string, v reflect.Value) []Pair {
+ return nil
+ }
+ }
+}
+
+func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc {
+ f, _ := t.FieldByName("Value")
+ enc := e.typeEncoder(f.Type)
+
+ return func(key string, value reflect.Value) []Pair {
+ present := value.FieldByName("Present")
+ if !present.Bool() {
+ return nil
+ }
+ null := value.FieldByName("Null")
+ if null.Bool() {
+ // TODO: Error?
+ return nil
+ }
+ raw := value.FieldByName("Raw")
+ if !raw.IsNil() {
+ return e.typeEncoder(raw.Type())(key, raw)
+ }
+ return enc(key, value.FieldByName("Value"))
+ }
+}
+
+func (e *encoder) newTimeTypeEncoder(t reflect.Type) encoderFunc {
+ format := e.dateFormat
+ return func(key string, value reflect.Value) []Pair {
+ return []Pair{{
+ key,
+ value.Convert(reflect.TypeOf(time.Time{})).Interface().(time.Time).Format(format),
+ }}
+ }
+}
+
+func (e encoder) newInterfaceEncoder() encoderFunc {
+ return func(key string, value reflect.Value) []Pair {
+ value = value.Elem()
+ if !value.IsValid() {
+ return nil
+ }
+ return e.typeEncoder(value.Type())(key, value)
+ }
+
+}
diff --git a/packages/tui/sdk/internal/apiquery/query.go b/packages/tui/sdk/internal/apiquery/query.go
new file mode 100644
index 000000000..6f90e9935
--- /dev/null
+++ b/packages/tui/sdk/internal/apiquery/query.go
@@ -0,0 +1,50 @@
+package apiquery
+
+import (
+ "net/url"
+ "reflect"
+ "time"
+)
+
+func MarshalWithSettings(value interface{}, settings QuerySettings) url.Values {
+ e := encoder{time.RFC3339, true, settings}
+ kv := url.Values{}
+ val := reflect.ValueOf(value)
+ if !val.IsValid() {
+ return nil
+ }
+ typ := val.Type()
+ for _, pair := range e.typeEncoder(typ)("", val) {
+ kv.Add(pair.key, pair.value)
+ }
+ return kv
+}
+
+func Marshal(value interface{}) url.Values {
+ return MarshalWithSettings(value, QuerySettings{})
+}
+
+type Queryer interface {
+ URLQuery() url.Values
+}
+
+type QuerySettings struct {
+ NestedFormat NestedQueryFormat
+ ArrayFormat ArrayQueryFormat
+}
+
+type NestedQueryFormat int
+
+const (
+ NestedQueryFormatBrackets NestedQueryFormat = iota
+ NestedQueryFormatDots
+)
+
+type ArrayQueryFormat int
+
+const (
+ ArrayQueryFormatComma ArrayQueryFormat = iota
+ ArrayQueryFormatRepeat
+ ArrayQueryFormatIndices
+ ArrayQueryFormatBrackets
+)
diff --git a/packages/tui/sdk/internal/apiquery/query_test.go b/packages/tui/sdk/internal/apiquery/query_test.go
new file mode 100644
index 000000000..1e740d6a9
--- /dev/null
+++ b/packages/tui/sdk/internal/apiquery/query_test.go
@@ -0,0 +1,335 @@
+package apiquery
+
+import (
+ "net/url"
+ "testing"
+ "time"
+)
+
+func P[T any](v T) *T { return &v }
+
+type Primitives struct {
+ A bool `query:"a"`
+ B int `query:"b"`
+ C uint `query:"c"`
+ D float64 `query:"d"`
+ E float32 `query:"e"`
+ F []int `query:"f"`
+}
+
+type PrimitivePointers struct {
+ A *bool `query:"a"`
+ B *int `query:"b"`
+ C *uint `query:"c"`
+ D *float64 `query:"d"`
+ E *float32 `query:"e"`
+ F *[]int `query:"f"`
+}
+
+type Slices struct {
+ Slice []Primitives `query:"slices"`
+ Mixed []interface{} `query:"mixed"`
+}
+
+type DateTime struct {
+ Date time.Time `query:"date" format:"date"`
+ DateTime time.Time `query:"date-time" format:"date-time"`
+}
+
+type AdditionalProperties struct {
+ A bool `query:"a"`
+ Extras map[string]interface{} `query:"-,inline"`
+}
+
+type Recursive struct {
+ Name string `query:"name"`
+ Child *Recursive `query:"child"`
+}
+
+type UnknownStruct struct {
+ Unknown interface{} `query:"unknown"`
+}
+
+type UnionStruct struct {
+ Union Union `query:"union" format:"date"`
+}
+
+type Union interface {
+ union()
+}
+
+type UnionInteger int64
+
+func (UnionInteger) union() {}
+
+type UnionString string
+
+func (UnionString) union() {}
+
+type UnionStructA struct {
+ Type string `query:"type"`
+ A string `query:"a"`
+ B string `query:"b"`
+}
+
+func (UnionStructA) union() {}
+
+type UnionStructB struct {
+ Type string `query:"type"`
+ A string `query:"a"`
+}
+
+func (UnionStructB) union() {}
+
+type UnionTime time.Time
+
+func (UnionTime) union() {}
+
+type DeeplyNested struct {
+ A DeeplyNested1 `query:"a"`
+}
+
+type DeeplyNested1 struct {
+ B DeeplyNested2 `query:"b"`
+}
+
+type DeeplyNested2 struct {
+ C DeeplyNested3 `query:"c"`
+}
+
+type DeeplyNested3 struct {
+ D *string `query:"d"`
+}
+
+var tests = map[string]struct {
+ enc string
+ val interface{}
+ settings QuerySettings
+}{
+ "primitives": {
+ "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4",
+ Primitives{A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ QuerySettings{},
+ },
+
+ "slices_brackets": {
+ `mixed[]=1&mixed[]=2.3&mixed[]=hello&slices[][a]=false&slices[][a]=false&slices[][b]=237628372683&slices[][b]=237628372683&slices[][c]=654&slices[][c]=654&slices[][d]=9999.43&slices[][d]=9999.43&slices[][e]=43.7599983215332&slices[][e]=43.7599983215332&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4&slices[][f][]=1&slices[][f][]=2&slices[][f][]=3&slices[][f][]=4`,
+ Slices{
+ Slice: []Primitives{
+ {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ },
+ Mixed: []interface{}{1, 2.3, "hello"},
+ },
+ QuerySettings{ArrayFormat: ArrayQueryFormatBrackets},
+ },
+
+ "slices_comma": {
+ `mixed=1,2.3,hello`,
+ Slices{
+ Mixed: []interface{}{1, 2.3, "hello"},
+ },
+ QuerySettings{ArrayFormat: ArrayQueryFormatComma},
+ },
+
+ "slices_repeat": {
+ `mixed=1&mixed=2.3&mixed=hello&slices[a]=false&slices[a]=false&slices[b]=237628372683&slices[b]=237628372683&slices[c]=654&slices[c]=654&slices[d]=9999.43&slices[d]=9999.43&slices[e]=43.7599983215332&slices[e]=43.7599983215332&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4&slices[f]=1&slices[f]=2&slices[f]=3&slices[f]=4`,
+ Slices{
+ Slice: []Primitives{
+ {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ {A: false, B: 237628372683, C: uint(654), D: 9999.43, E: 43.76, F: []int{1, 2, 3, 4}},
+ },
+ Mixed: []interface{}{1, 2.3, "hello"},
+ },
+ QuerySettings{ArrayFormat: ArrayQueryFormatRepeat},
+ },
+
+ "primitive_pointer_struct": {
+ "a=false&b=237628372683&c=654&d=9999.43&e=43.7599983215332&f=1,2,3,4,5",
+ PrimitivePointers{
+ A: P(false),
+ B: P(237628372683),
+ C: P(uint(654)),
+ D: P(9999.43),
+ E: P(float32(43.76)),
+ F: &[]int{1, 2, 3, 4, 5},
+ },
+ QuerySettings{},
+ },
+
+ "datetime_struct": {
+ `date=2006-01-02&date-time=2006-01-02T15:04:05Z`,
+ DateTime{
+ Date: time.Date(2006, time.January, 2, 0, 0, 0, 0, time.UTC),
+ DateTime: time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC),
+ },
+ QuerySettings{},
+ },
+
+ "additional_properties": {
+ `a=true&bar=value&foo=true`,
+ AdditionalProperties{
+ A: true,
+ Extras: map[string]interface{}{
+ "bar": "value",
+ "foo": true,
+ },
+ },
+ QuerySettings{},
+ },
+
+ "recursive_struct_brackets": {
+ `child[name]=Alex&name=Robert`,
+ Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
+ QuerySettings{NestedFormat: NestedQueryFormatBrackets},
+ },
+
+ "recursive_struct_dots": {
+ `child.name=Alex&name=Robert`,
+ Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}},
+ QuerySettings{NestedFormat: NestedQueryFormatDots},
+ },
+
+ "unknown_struct_number": {
+ `unknown=12`,
+ UnknownStruct{
+ Unknown: 12.,
+ },
+ QuerySettings{},
+ },
+
+ "unknown_struct_map_brackets": {
+ `unknown[foo]=bar`,
+ UnknownStruct{
+ Unknown: map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ QuerySettings{NestedFormat: NestedQueryFormatBrackets},
+ },
+
+ "unknown_struct_map_dots": {
+ `unknown.foo=bar`,
+ UnknownStruct{
+ Unknown: map[string]interface{}{
+ "foo": "bar",
+ },
+ },
+ QuerySettings{NestedFormat: NestedQueryFormatDots},
+ },
+
+ "union_string": {
+ `union=hello`,
+ UnionStruct{
+ Union: UnionString("hello"),
+ },
+ QuerySettings{},
+ },
+
+ "union_integer": {
+ `union=12`,
+ UnionStruct{
+ Union: UnionInteger(12),
+ },
+ QuerySettings{},
+ },
+
+ "union_struct_discriminated_a": {
+ `union[a]=foo&union[b]=bar&union[type]=typeA`,
+ UnionStruct{
+ Union: UnionStructA{
+ Type: "typeA",
+ A: "foo",
+ B: "bar",
+ },
+ },
+ QuerySettings{},
+ },
+
+ "union_struct_discriminated_b": {
+ `union[a]=foo&union[type]=typeB`,
+ UnionStruct{
+ Union: UnionStructB{
+ Type: "typeB",
+ A: "foo",
+ },
+ },
+ QuerySettings{},
+ },
+
+ "union_struct_time": {
+ `union=2010-05-23`,
+ UnionStruct{
+ Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)),
+ },
+ QuerySettings{},
+ },
+
+ "deeply_nested_brackets": {
+ `a[b][c][d]=hello`,
+ DeeplyNested{
+ A: DeeplyNested1{
+ B: DeeplyNested2{
+ C: DeeplyNested3{
+ D: P("hello"),
+ },
+ },
+ },
+ },
+ QuerySettings{NestedFormat: NestedQueryFormatBrackets},
+ },
+
+ "deeply_nested_dots": {
+ `a.b.c.d=hello`,
+ DeeplyNested{
+ A: DeeplyNested1{
+ B: DeeplyNested2{
+ C: DeeplyNested3{
+ D: P("hello"),
+ },
+ },
+ },
+ },
+ QuerySettings{NestedFormat: NestedQueryFormatDots},
+ },
+
+ "deeply_nested_brackets_empty": {
+ ``,
+ DeeplyNested{
+ A: DeeplyNested1{
+ B: DeeplyNested2{
+ C: DeeplyNested3{
+ D: nil,
+ },
+ },
+ },
+ },
+ QuerySettings{NestedFormat: NestedQueryFormatBrackets},
+ },
+
+ "deeply_nested_dots_empty": {
+ ``,
+ DeeplyNested{
+ A: DeeplyNested1{
+ B: DeeplyNested2{
+ C: DeeplyNested3{
+ D: nil,
+ },
+ },
+ },
+ },
+ QuerySettings{NestedFormat: NestedQueryFormatDots},
+ },
+}
+
+func TestEncode(t *testing.T) {
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ values := MarshalWithSettings(test.val, test.settings)
+ str, _ := url.QueryUnescape(values.Encode())
+ if str != test.enc {
+ t.Fatalf("expected %+#v to serialize to %s but got %s", test.val, test.enc, str)
+ }
+ })
+ }
+}
diff --git a/packages/tui/sdk/internal/apiquery/tag.go b/packages/tui/sdk/internal/apiquery/tag.go
new file mode 100644
index 000000000..7ccd739c2
--- /dev/null
+++ b/packages/tui/sdk/internal/apiquery/tag.go
@@ -0,0 +1,41 @@
+package apiquery
+
+import (
+ "reflect"
+ "strings"
+)
+
+const queryStructTag = "query"
+const formatStructTag = "format"
+
+type parsedStructTag struct {
+ name string
+ omitempty bool
+ inline bool
+}
+
+func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) {
+ raw, ok := field.Tag.Lookup(queryStructTag)
+ if !ok {
+ return
+ }
+ parts := strings.Split(raw, ",")
+ if len(parts) == 0 {
+ return tag, false
+ }
+ tag.name = parts[0]
+ for _, part := range parts[1:] {
+ switch part {
+ case "omitempty":
+ tag.omitempty = true
+ case "inline":
+ tag.inline = true
+ }
+ }
+ return
+}
+
+func parseFormatStructTag(field reflect.StructField) (format string, ok bool) {
+ format, ok = field.Tag.Lookup(formatStructTag)
+ return
+}
diff --git a/packages/tui/sdk/internal/param/field.go b/packages/tui/sdk/internal/param/field.go
new file mode 100644
index 000000000..4d0fd9c6f
--- /dev/null
+++ b/packages/tui/sdk/internal/param/field.go
@@ -0,0 +1,29 @@
+package param
+
+import (
+ "fmt"
+)
+
+type FieldLike interface{ field() }
+
+// Field is a wrapper used for all values sent to the API,
+// to distinguish zero values from null or omitted fields.
+//
+// It also allows sending arbitrary deserializable values.
+//
+// To instantiate a Field, use the helpers exported from
+// the package root: `F()`, `Null()`, `Raw()`, etc.
+type Field[T any] struct {
+ FieldLike
+ Value T
+ Null bool
+ Present bool
+ Raw any
+}
+
+func (f Field[T]) String() string {
+ if s, ok := any(f.Value).(fmt.Stringer); ok {
+ return s.String()
+ }
+ return fmt.Sprintf("%v", f.Value)
+}
diff --git a/packages/tui/sdk/internal/requestconfig/requestconfig.go b/packages/tui/sdk/internal/requestconfig/requestconfig.go
new file mode 100644
index 000000000..91b70cca5
--- /dev/null
+++ b/packages/tui/sdk/internal/requestconfig/requestconfig.go
@@ -0,0 +1,629 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package requestconfig
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "math/rand"
+ "mime"
+ "net/http"
+ "net/url"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/sst/opencode-sdk-go/internal"
+ "github.com/sst/opencode-sdk-go/internal/apierror"
+ "github.com/sst/opencode-sdk-go/internal/apiform"
+ "github.com/sst/opencode-sdk-go/internal/apiquery"
+ "github.com/sst/opencode-sdk-go/internal/param"
+)
+
+func getDefaultHeaders() map[string]string {
+ return map[string]string{
+ "User-Agent": fmt.Sprintf("Opencode/Go %s", internal.PackageVersion),
+ }
+}
+
+func getNormalizedOS() string {
+ switch runtime.GOOS {
+ case "ios":
+ return "iOS"
+ case "android":
+ return "Android"
+ case "darwin":
+ return "MacOS"
+ case "window":
+ return "Windows"
+ case "freebsd":
+ return "FreeBSD"
+ case "openbsd":
+ return "OpenBSD"
+ case "linux":
+ return "Linux"
+ default:
+ return fmt.Sprintf("Other:%s", runtime.GOOS)
+ }
+}
+
+func getNormalizedArchitecture() string {
+ switch runtime.GOARCH {
+ case "386":
+ return "x32"
+ case "amd64":
+ return "x64"
+ case "arm":
+ return "arm"
+ case "arm64":
+ return "arm64"
+ default:
+ return fmt.Sprintf("other:%s", runtime.GOARCH)
+ }
+}
+
+func getPlatformProperties() map[string]string {
+ return map[string]string{
+ "X-Stainless-Lang": "go",
+ "X-Stainless-Package-Version": internal.PackageVersion,
+ "X-Stainless-OS": getNormalizedOS(),
+ "X-Stainless-Arch": getNormalizedArchitecture(),
+ "X-Stainless-Runtime": "go",
+ "X-Stainless-Runtime-Version": runtime.Version(),
+ }
+}
+
+type RequestOption interface {
+ Apply(*RequestConfig) error
+}
+
+type RequestOptionFunc func(*RequestConfig) error
+type PreRequestOptionFunc func(*RequestConfig) error
+
+func (s RequestOptionFunc) Apply(r *RequestConfig) error { return s(r) }
+func (s PreRequestOptionFunc) Apply(r *RequestConfig) error { return s(r) }
+
+func NewRequestConfig(ctx context.Context, method string, u string, body interface{}, dst interface{}, opts ...RequestOption) (*RequestConfig, error) {
+ var reader io.Reader
+
+ contentType := "application/json"
+ hasSerializationFunc := false
+
+ if body, ok := body.(json.Marshaler); ok {
+ content, err := body.MarshalJSON()
+ if err != nil {
+ return nil, err
+ }
+ reader = bytes.NewBuffer(content)
+ hasSerializationFunc = true
+ }
+ if body, ok := body.(apiform.Marshaler); ok {
+ var (
+ content []byte
+ err error
+ )
+ content, contentType, err = body.MarshalMultipart()
+ if err != nil {
+ return nil, err
+ }
+ reader = bytes.NewBuffer(content)
+ hasSerializationFunc = true
+ }
+ if body, ok := body.(apiquery.Queryer); ok {
+ hasSerializationFunc = true
+ params := body.URLQuery().Encode()
+ if params != "" {
+ u = u + "?" + params
+ }
+ }
+ if body, ok := body.([]byte); ok {
+ reader = bytes.NewBuffer(body)
+ hasSerializationFunc = true
+ }
+ if body, ok := body.(io.Reader); ok {
+ reader = body
+ hasSerializationFunc = true
+ }
+
+ // Fallback to json serialization if none of the serialization functions that we expect
+ // to see is present.
+ if body != nil && !hasSerializationFunc {
+ content, err := json.Marshal(body)
+ if err != nil {
+ return nil, err
+ }
+ reader = bytes.NewBuffer(content)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, u, nil)
+ if err != nil {
+ return nil, err
+ }
+ if reader != nil {
+ req.Header.Set("Content-Type", contentType)
+ }
+
+ req.Header.Set("Accept", "application/json")
+ req.Header.Set("X-Stainless-Retry-Count", "0")
+ req.Header.Set("X-Stainless-Timeout", "0")
+ for k, v := range getDefaultHeaders() {
+ req.Header.Add(k, v)
+ }
+
+ for k, v := range getPlatformProperties() {
+ req.Header.Add(k, v)
+ }
+ cfg := RequestConfig{
+ MaxRetries: 2,
+ Context: ctx,
+ Request: req,
+ HTTPClient: http.DefaultClient,
+ Body: reader,
+ }
+ cfg.ResponseBodyInto = dst
+ err = cfg.Apply(opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ // This must run after `cfg.Apply(...)` above in case the request timeout gets modified. We also only
+ // apply our own logic for it if it's still "0" from above. If it's not, then it was deleted or modified
+ // by the user and we should respect that.
+ if req.Header.Get("X-Stainless-Timeout") == "0" {
+ if cfg.RequestTimeout == time.Duration(0) {
+ req.Header.Del("X-Stainless-Timeout")
+ } else {
+ req.Header.Set("X-Stainless-Timeout", strconv.Itoa(int(cfg.RequestTimeout.Seconds())))
+ }
+ }
+
+ return &cfg, nil
+}
+
+func UseDefaultParam[T any](dst *param.Field[T], src *T) {
+ if !dst.Present && src != nil {
+ dst.Value = *src
+ dst.Present = true
+ }
+}
+
+// This interface is primarily used to describe an [*http.Client], but also
+// supports custom HTTP implementations.
+type HTTPDoer interface {
+ Do(req *http.Request) (*http.Response, error)
+}
+
+// RequestConfig represents all the state related to one request.
+//
+// Editing the variables inside RequestConfig directly is unstable api. Prefer
+// composing the RequestOption instead if possible.
+type RequestConfig struct {
+ MaxRetries int
+ RequestTimeout time.Duration
+ Context context.Context
+ Request *http.Request
+ BaseURL *url.URL
+ // DefaultBaseURL will be used if BaseURL is not explicitly overridden using
+ // WithBaseURL.
+ DefaultBaseURL *url.URL
+ CustomHTTPDoer HTTPDoer
+ HTTPClient *http.Client
+ Middlewares []middleware
+ // If ResponseBodyInto not nil, then we will attempt to deserialize into
+ // ResponseBodyInto. If Destination is a []byte, then it will return the body as
+ // is.
+ ResponseBodyInto interface{}
+ // ResponseInto copies the \*http.Response of the corresponding request into the
+ // given address
+ ResponseInto **http.Response
+ Body io.Reader
+}
+
+// middleware is exactly the same type as the Middleware type found in the [option] package,
+// but it is redeclared here for circular dependency issues.
+type middleware = func(*http.Request, middlewareNext) (*http.Response, error)
+
+// middlewareNext is exactly the same type as the MiddlewareNext type found in the [option] package,
+// but it is redeclared here for circular dependency issues.
+type middlewareNext = func(*http.Request) (*http.Response, error)
+
+func applyMiddleware(middleware middleware, next middlewareNext) middlewareNext {
+ return func(req *http.Request) (res *http.Response, err error) {
+ return middleware(req, next)
+ }
+}
+
+func shouldRetry(req *http.Request, res *http.Response) bool {
+ // If there is no way to recover the Body, then we shouldn't retry.
+ if req.Body != nil && req.GetBody == nil {
+ return false
+ }
+
+ // If there is no response, that indicates that there is a connection error
+ // so we retry the request.
+ if res == nil {
+ return true
+ }
+
+ // If the header explicitly wants a retry behavior, respect that over the
+ // http status code.
+ if res.Header.Get("x-should-retry") == "true" {
+ return true
+ }
+ if res.Header.Get("x-should-retry") == "false" {
+ return false
+ }
+
+ return res.StatusCode == http.StatusRequestTimeout ||
+ res.StatusCode == http.StatusConflict ||
+ res.StatusCode == http.StatusTooManyRequests ||
+ res.StatusCode >= http.StatusInternalServerError
+}
+
+func parseRetryAfterHeader(resp *http.Response) (time.Duration, bool) {
+ if resp == nil {
+ return 0, false
+ }
+
+ type retryData struct {
+ header string
+ units time.Duration
+
+ // custom is used when the regular algorithm failed and is optional.
+ // the returned duration is used verbatim (units is not applied).
+ custom func(string) (time.Duration, bool)
+ }
+
+ nop := func(string) (time.Duration, bool) { return 0, false }
+
+ // the headers are listed in order of preference
+ retries := []retryData{
+ {
+ header: "Retry-After-Ms",
+ units: time.Millisecond,
+ custom: nop,
+ },
+ {
+ header: "Retry-After",
+ units: time.Second,
+
+ // retry-after values are expressed in either number of
+ // seconds or an HTTP-date indicating when to try again
+ custom: func(ra string) (time.Duration, bool) {
+ t, err := time.Parse(time.RFC1123, ra)
+ if err != nil {
+ return 0, false
+ }
+ return time.Until(t), true
+ },
+ },
+ }
+
+ for _, retry := range retries {
+ v := resp.Header.Get(retry.header)
+ if v == "" {
+ continue
+ }
+ if retryAfter, err := strconv.ParseFloat(v, 64); err == nil {
+ return time.Duration(retryAfter * float64(retry.units)), true
+ }
+ if d, ok := retry.custom(v); ok {
+ return d, true
+ }
+ }
+
+ return 0, false
+}
+
+// isBeforeContextDeadline reports whether the non-zero Time t is
+// before ctx's deadline. If ctx does not have a deadline, it
+// always reports true (the deadline is considered infinite).
+func isBeforeContextDeadline(t time.Time, ctx context.Context) bool {
+ d, ok := ctx.Deadline()
+ if !ok {
+ return true
+ }
+ return t.Before(d)
+}
+
+// bodyWithTimeout is an io.ReadCloser which can observe a context's cancel func
+// to handle timeouts etc. It wraps an existing io.ReadCloser.
+type bodyWithTimeout struct {
+ stop func() // stops the time.Timer waiting to cancel the request
+ rc io.ReadCloser
+}
+
+func (b *bodyWithTimeout) Read(p []byte) (n int, err error) {
+ n, err = b.rc.Read(p)
+ if err == nil {
+ return n, nil
+ }
+ if err == io.EOF {
+ return n, err
+ }
+ return n, err
+}
+
+func (b *bodyWithTimeout) Close() error {
+ err := b.rc.Close()
+ b.stop()
+ return err
+}
+
+func retryDelay(res *http.Response, retryCount int) time.Duration {
+ // If the API asks us to wait a certain amount of time (and it's a reasonable amount),
+ // just do what it says.
+
+ if retryAfterDelay, ok := parseRetryAfterHeader(res); ok && 0 <= retryAfterDelay && retryAfterDelay < time.Minute {
+ return retryAfterDelay
+ }
+
+ maxDelay := 8 * time.Second
+ delay := time.Duration(0.5 * float64(time.Second) * math.Pow(2, float64(retryCount)))
+ if delay > maxDelay {
+ delay = maxDelay
+ }
+
+ jitter := rand.Int63n(int64(delay / 4))
+ delay -= time.Duration(jitter)
+ return delay
+}
+
+func (cfg *RequestConfig) Execute() (err error) {
+ if cfg.BaseURL == nil {
+ if cfg.DefaultBaseURL != nil {
+ cfg.BaseURL = cfg.DefaultBaseURL
+ } else {
+ return fmt.Errorf("requestconfig: base url is not set")
+ }
+ }
+
+ cfg.Request.URL, err = cfg.BaseURL.Parse(strings.TrimLeft(cfg.Request.URL.String(), "/"))
+ if err != nil {
+ return err
+ }
+
+ if cfg.Body != nil && cfg.Request.Body == nil {
+ switch body := cfg.Body.(type) {
+ case *bytes.Buffer:
+ b := body.Bytes()
+ cfg.Request.ContentLength = int64(body.Len())
+ cfg.Request.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(b)), nil }
+ cfg.Request.Body, _ = cfg.Request.GetBody()
+ case *bytes.Reader:
+ cfg.Request.ContentLength = int64(body.Len())
+ cfg.Request.GetBody = func() (io.ReadCloser, error) {
+ _, err := body.Seek(0, 0)
+ return io.NopCloser(body), err
+ }
+ cfg.Request.Body, _ = cfg.Request.GetBody()
+ default:
+ if rc, ok := body.(io.ReadCloser); ok {
+ cfg.Request.Body = rc
+ } else {
+ cfg.Request.Body = io.NopCloser(body)
+ }
+ }
+ }
+
+ handler := cfg.HTTPClient.Do
+ if cfg.CustomHTTPDoer != nil {
+ handler = cfg.CustomHTTPDoer.Do
+ }
+ for i := len(cfg.Middlewares) - 1; i >= 0; i -= 1 {
+ handler = applyMiddleware(cfg.Middlewares[i], handler)
+ }
+
+ // Don't send the current retry count in the headers if the caller modified the header defaults.
+ shouldSendRetryCount := cfg.Request.Header.Get("X-Stainless-Retry-Count") == "0"
+
+ var res *http.Response
+ var cancel context.CancelFunc
+ for retryCount := 0; retryCount <= cfg.MaxRetries; retryCount += 1 {
+ ctx := cfg.Request.Context()
+ if cfg.RequestTimeout != time.Duration(0) && isBeforeContextDeadline(time.Now().Add(cfg.RequestTimeout), ctx) {
+ ctx, cancel = context.WithTimeout(ctx, cfg.RequestTimeout)
+ defer func() {
+ // The cancel function is nil if it was handed off to be handled in a different scope.
+ if cancel != nil {
+ cancel()
+ }
+ }()
+ }
+
+ req := cfg.Request.Clone(ctx)
+ if shouldSendRetryCount {
+ req.Header.Set("X-Stainless-Retry-Count", strconv.Itoa(retryCount))
+ }
+
+ res, err = handler(req)
+ if ctx != nil && ctx.Err() != nil {
+ return ctx.Err()
+ }
+ if !shouldRetry(cfg.Request, res) || retryCount >= cfg.MaxRetries {
+ break
+ }
+
+ // Prepare next request and wait for the retry delay
+ if cfg.Request.GetBody != nil {
+ cfg.Request.Body, err = cfg.Request.GetBody()
+ if err != nil {
+ return err
+ }
+ }
+
+ // Can't actually refresh the body, so we don't attempt to retry here
+ if cfg.Request.GetBody == nil && cfg.Request.Body != nil {
+ break
+ }
+
+ time.Sleep(retryDelay(res, retryCount))
+ }
+
+ // Save *http.Response if it is requested to, even if there was an error making the request. This is
+ // useful in cases where you might want to debug by inspecting the response. Note that if err != nil,
+ // the response should be generally be empty, but there are edge cases.
+ if cfg.ResponseInto != nil {
+ *cfg.ResponseInto = res
+ }
+ if responseBodyInto, ok := cfg.ResponseBodyInto.(**http.Response); ok {
+ *responseBodyInto = res
+ }
+
+ // If there was a connection error in the final request or any other transport error,
+ // return that early without trying to coerce into an APIError.
+ if err != nil {
+ return err
+ }
+
+ if res.StatusCode >= 400 {
+ contents, err := io.ReadAll(res.Body)
+ res.Body.Close()
+ if err != nil {
+ return err
+ }
+
+ // If there is an APIError, re-populate the response body so that debugging
+ // utilities can conveniently dump the response without issue.
+ res.Body = io.NopCloser(bytes.NewBuffer(contents))
+
+ // Load the contents into the error format if it is provided.
+ aerr := apierror.Error{Request: cfg.Request, Response: res, StatusCode: res.StatusCode}
+ err = aerr.UnmarshalJSON(contents)
+ if err != nil {
+ return err
+ }
+ return &aerr
+ }
+
+ _, intoCustomResponseBody := cfg.ResponseBodyInto.(**http.Response)
+ if cfg.ResponseBodyInto == nil || intoCustomResponseBody {
+ // We aren't reading the response body in this scope, but whoever is will need the
+ // cancel func from the context to observe request timeouts.
+ // Put the cancel function in the response body so it can be handled elsewhere.
+ if cancel != nil {
+ res.Body = &bodyWithTimeout{rc: res.Body, stop: cancel}
+ cancel = nil
+ }
+ return nil
+ }
+
+ contents, err := io.ReadAll(res.Body)
+ res.Body.Close()
+ if err != nil {
+ return fmt.Errorf("error reading response body: %w", err)
+ }
+
+ // If we are not json, return plaintext
+ contentType := res.Header.Get("content-type")
+ mediaType, _, _ := mime.ParseMediaType(contentType)
+ isJSON := strings.Contains(mediaType, "application/json") || strings.HasSuffix(mediaType, "+json")
+ if !isJSON {
+ switch dst := cfg.ResponseBodyInto.(type) {
+ case *string:
+ *dst = string(contents)
+ case **string:
+ tmp := string(contents)
+ *dst = &tmp
+ case *[]byte:
+ *dst = contents
+ default:
+ return fmt.Errorf("expected destination type of 'string' or '[]byte' for responses with content-type '%s' that is not 'application/json'", contentType)
+ }
+ return nil
+ }
+
+ switch dst := cfg.ResponseBodyInto.(type) {
+ // If the response happens to be a byte array, deserialize the body as-is.
+ case *[]byte:
+ *dst = contents
+ default:
+ err = json.NewDecoder(bytes.NewReader(contents)).Decode(cfg.ResponseBodyInto)
+ if err != nil {
+ return fmt.Errorf("error parsing response json: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func ExecuteNewRequest(ctx context.Context, method string, u string, body interface{}, dst interface{}, opts ...RequestOption) error {
+ cfg, err := NewRequestConfig(ctx, method, u, body, dst, opts...)
+ if err != nil {
+ return err
+ }
+ return cfg.Execute()
+}
+
+func (cfg *RequestConfig) Clone(ctx context.Context) *RequestConfig {
+ if cfg == nil {
+ return nil
+ }
+ req := cfg.Request.Clone(ctx)
+ var err error
+ if req.Body != nil {
+ req.Body, err = req.GetBody()
+ }
+ if err != nil {
+ return nil
+ }
+ new := &RequestConfig{
+ MaxRetries: cfg.MaxRetries,
+ RequestTimeout: cfg.RequestTimeout,
+ Context: ctx,
+ Request: req,
+ BaseURL: cfg.BaseURL,
+ HTTPClient: cfg.HTTPClient,
+ Middlewares: cfg.Middlewares,
+ }
+
+ return new
+}
+
+func (cfg *RequestConfig) Apply(opts ...RequestOption) error {
+ for _, opt := range opts {
+ err := opt.Apply(cfg)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// PreRequestOptions is used to collect all the options which need to be known before
+// a call to [RequestConfig.ExecuteNewRequest], such as path parameters
+// or global defaults.
+// PreRequestOptions will return a [RequestConfig] with the options applied.
+//
+// Only request option functions of type [PreRequestOptionFunc] are applied.
+func PreRequestOptions(opts ...RequestOption) (RequestConfig, error) {
+ cfg := RequestConfig{}
+ for _, opt := range opts {
+ if opt, ok := opt.(PreRequestOptionFunc); ok {
+ err := opt.Apply(&cfg)
+ if err != nil {
+ return cfg, err
+ }
+ }
+ }
+ return cfg, nil
+}
+
+// WithDefaultBaseURL returns a RequestOption that sets the client's default Base URL.
+// This is always overridden by setting a base URL with WithBaseURL.
+// WithBaseURL should be used instead of WithDefaultBaseURL except in internal code.
+func WithDefaultBaseURL(baseURL string) RequestOption {
+ u, err := url.Parse(baseURL)
+ return RequestOptionFunc(func(r *RequestConfig) error {
+ if err != nil {
+ return err
+ }
+ r.DefaultBaseURL = u
+ return nil
+ })
+}
diff --git a/packages/tui/sdk/internal/testutil/testutil.go b/packages/tui/sdk/internal/testutil/testutil.go
new file mode 100644
index 000000000..826d266f2
--- /dev/null
+++ b/packages/tui/sdk/internal/testutil/testutil.go
@@ -0,0 +1,27 @@
+package testutil
+
+import (
+ "net/http"
+ "os"
+ "strconv"
+ "testing"
+)
+
+func CheckTestServer(t *testing.T, url string) bool {
+ if _, err := http.Get(url); err != nil {
+ const SKIP_MOCK_TESTS = "SKIP_MOCK_TESTS"
+ if str, ok := os.LookupEnv(SKIP_MOCK_TESTS); ok {
+ skip, err := strconv.ParseBool(str)
+ if err != nil {
+ t.Fatalf("strconv.ParseBool(os.LookupEnv(%s)) failed: %s", SKIP_MOCK_TESTS, err)
+ }
+ if skip {
+ t.Skip("The test will not run without a mock Prism server running against your OpenAPI spec")
+ return false
+ }
+ t.Errorf("The test will not run without a mock Prism server running against your OpenAPI spec. You can set the environment variable %s to true to skip running any tests that require the mock server", SKIP_MOCK_TESTS)
+ return false
+ }
+ }
+ return true
+}
diff --git a/packages/tui/sdk/internal/version.go b/packages/tui/sdk/internal/version.go
new file mode 100644
index 000000000..64dcebbb4
--- /dev/null
+++ b/packages/tui/sdk/internal/version.go
@@ -0,0 +1,5 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package internal
+
+const PackageVersion = "0.1.0-alpha.8" // x-release-please-version
diff --git a/packages/tui/sdk/lib/.keep b/packages/tui/sdk/lib/.keep
new file mode 100644
index 000000000..5e2c99fdb
--- /dev/null
+++ b/packages/tui/sdk/lib/.keep
@@ -0,0 +1,4 @@
+File generated from our OpenAPI spec by Stainless.
+
+This directory can be used to store custom files to expand the SDK.
+It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file
diff --git a/packages/tui/sdk/option/middleware.go b/packages/tui/sdk/option/middleware.go
new file mode 100644
index 000000000..8ec9dd60b
--- /dev/null
+++ b/packages/tui/sdk/option/middleware.go
@@ -0,0 +1,38 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package option
+
+import (
+ "log"
+ "net/http"
+ "net/http/httputil"
+)
+
+// WithDebugLog logs the HTTP request and response content.
+// If the logger parameter is nil, it uses the default logger.
+//
+// WithDebugLog is for debugging and development purposes only.
+// It should not be used in production code. The behavior and interface
+// of WithDebugLog is not guaranteed to be stable.
+func WithDebugLog(logger *log.Logger) RequestOption {
+ return WithMiddleware(func(req *http.Request, nxt MiddlewareNext) (*http.Response, error) {
+ if logger == nil {
+ logger = log.Default()
+ }
+
+ if reqBytes, err := httputil.DumpRequest(req, true); err == nil {
+ logger.Printf("Request Content:\n%s\n", reqBytes)
+ }
+
+ resp, err := nxt(req)
+ if err != nil {
+ return resp, err
+ }
+
+ if respBytes, err := httputil.DumpResponse(resp, true); err == nil {
+ logger.Printf("Response Content:\n%s\n", respBytes)
+ }
+
+ return resp, err
+ })
+}
diff --git a/packages/tui/sdk/option/requestoption.go b/packages/tui/sdk/option/requestoption.go
new file mode 100644
index 000000000..313552e9b
--- /dev/null
+++ b/packages/tui/sdk/option/requestoption.go
@@ -0,0 +1,266 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package option
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/tidwall/sjson"
+)
+
+// RequestOption is an option for the requests made by the opencode API Client
+// which can be supplied to clients, services, and methods. You can read more about this functional
+// options pattern in our [README].
+//
+// [README]: https://pkg.go.dev/github.com/sst/opencode-sdk-go#readme-requestoptions
+type RequestOption = requestconfig.RequestOption
+
+// WithBaseURL returns a RequestOption that sets the BaseURL for the client.
+//
+// For security reasons, ensure that the base URL is trusted.
+func WithBaseURL(base string) RequestOption {
+ u, err := url.Parse(base)
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ if err != nil {
+ return fmt.Errorf("requestoption: WithBaseURL failed to parse url %s\n", err)
+ }
+
+ if u.Path != "" && !strings.HasSuffix(u.Path, "/") {
+ u.Path += "/"
+ }
+ r.BaseURL = u
+ return nil
+ })
+}
+
+// HTTPClient is primarily used to describe an [*http.Client], but also
+// supports custom implementations.
+//
+// For bespoke implementations, prefer using an [*http.Client] with a
+// custom transport. See [http.RoundTripper] for further information.
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// WithHTTPClient returns a RequestOption that changes the underlying http client used to make this
+// request, which by default is [http.DefaultClient].
+//
+// For custom uses cases, it is recommended to provide an [*http.Client] with a custom
+// [http.RoundTripper] as its transport, rather than directly implementing [HTTPClient].
+func WithHTTPClient(client HTTPClient) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ if client == nil {
+ return fmt.Errorf("requestoption: custom http client cannot be nil")
+ }
+
+ if c, ok := client.(*http.Client); ok {
+ // Prefer the native client if possible.
+ r.HTTPClient = c
+ r.CustomHTTPDoer = nil
+ } else {
+ r.CustomHTTPDoer = client
+ }
+
+ return nil
+ })
+}
+
+// MiddlewareNext is a function which is called by a middleware to pass an HTTP request
+// to the next stage in the middleware chain.
+type MiddlewareNext = func(*http.Request) (*http.Response, error)
+
+// Middleware is a function which intercepts HTTP requests, processing or modifying
+// them, and then passing the request to the next middleware or handler
+// in the chain by calling the provided MiddlewareNext function.
+type Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error)
+
+// WithMiddleware returns a RequestOption that applies the given middleware
+// to the requests made. Each middleware will execute in the order they were given.
+func WithMiddleware(middlewares ...Middleware) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.Middlewares = append(r.Middlewares, middlewares...)
+ return nil
+ })
+}
+
+// WithMaxRetries returns a RequestOption that sets the maximum number of retries that the client
+// attempts to make. When given 0, the client only makes one request. By
+// default, the client retries two times.
+//
+// WithMaxRetries panics when retries is negative.
+func WithMaxRetries(retries int) RequestOption {
+ if retries < 0 {
+ panic("option: cannot have fewer than 0 retries")
+ }
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.MaxRetries = retries
+ return nil
+ })
+}
+
+// WithHeader returns a RequestOption that sets the header value to the associated key. It overwrites
+// any value if there was one already present.
+func WithHeader(key, value string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.Request.Header.Set(key, value)
+ return nil
+ })
+}
+
+// WithHeaderAdd returns a RequestOption that adds the header value to the associated key. It appends
+// onto any existing values.
+func WithHeaderAdd(key, value string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.Request.Header.Add(key, value)
+ return nil
+ })
+}
+
+// WithHeaderDel returns a RequestOption that deletes the header value(s) associated with the given key.
+func WithHeaderDel(key string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.Request.Header.Del(key)
+ return nil
+ })
+}
+
+// WithQuery returns a RequestOption that sets the query value to the associated key. It overwrites
+// any value if there was one already present.
+func WithQuery(key, value string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ query := r.Request.URL.Query()
+ query.Set(key, value)
+ r.Request.URL.RawQuery = query.Encode()
+ return nil
+ })
+}
+
+// WithQueryAdd returns a RequestOption that adds the query value to the associated key. It appends
+// onto any existing values.
+func WithQueryAdd(key, value string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ query := r.Request.URL.Query()
+ query.Add(key, value)
+ r.Request.URL.RawQuery = query.Encode()
+ return nil
+ })
+}
+
+// WithQueryDel returns a RequestOption that deletes the query value(s) associated with the key.
+func WithQueryDel(key string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ query := r.Request.URL.Query()
+ query.Del(key)
+ r.Request.URL.RawQuery = query.Encode()
+ return nil
+ })
+}
+
+// WithJSONSet returns a RequestOption that sets the body's JSON value associated with the key.
+// The key accepts a string as defined by the [sjson format].
+//
+// [sjson format]: https://github.com/tidwall/sjson
+func WithJSONSet(key string, value interface{}) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) {
+ var b []byte
+
+ if r.Body == nil {
+ b, err = sjson.SetBytes(nil, key, value)
+ if err != nil {
+ return err
+ }
+ } else if buffer, ok := r.Body.(*bytes.Buffer); ok {
+ b = buffer.Bytes()
+ b, err = sjson.SetBytes(b, key, value)
+ if err != nil {
+ return err
+ }
+ } else {
+ return fmt.Errorf("cannot use WithJSONSet on a body that is not serialized as *bytes.Buffer")
+ }
+
+ r.Body = bytes.NewBuffer(b)
+ return nil
+ })
+}
+
+// WithJSONDel returns a RequestOption that deletes the body's JSON value associated with the key.
+// The key accepts a string as defined by the [sjson format].
+//
+// [sjson format]: https://github.com/tidwall/sjson
+func WithJSONDel(key string) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) (err error) {
+ if buffer, ok := r.Body.(*bytes.Buffer); ok {
+ b := buffer.Bytes()
+ b, err = sjson.DeleteBytes(b, key)
+ if err != nil {
+ return err
+ }
+ r.Body = bytes.NewBuffer(b)
+ return nil
+ }
+
+ return fmt.Errorf("cannot use WithJSONDel on a body that is not serialized as *bytes.Buffer")
+ })
+}
+
+// WithResponseBodyInto returns a RequestOption that overwrites the deserialization target with
+// the given destination. If provided, we don't deserialize into the default struct.
+func WithResponseBodyInto(dst any) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.ResponseBodyInto = dst
+ return nil
+ })
+}
+
+// WithResponseInto returns a RequestOption that copies the [*http.Response] into the given address.
+func WithResponseInto(dst **http.Response) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.ResponseInto = dst
+ return nil
+ })
+}
+
+// WithRequestBody returns a RequestOption that provides a custom serialized body with the given
+// content type.
+//
+// body accepts an io.Reader or raw []bytes.
+func WithRequestBody(contentType string, body any) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ if reader, ok := body.(io.Reader); ok {
+ r.Body = reader
+ return r.Apply(WithHeader("Content-Type", contentType))
+ }
+
+ if b, ok := body.([]byte); ok {
+ r.Body = bytes.NewBuffer(b)
+ return r.Apply(WithHeader("Content-Type", contentType))
+ }
+
+ return fmt.Errorf("body must be a byte slice or implement io.Reader")
+ })
+}
+
+// WithRequestTimeout returns a RequestOption that sets the timeout for
+// each request attempt. This should be smaller than the timeout defined in
+// the context, which spans all retries.
+func WithRequestTimeout(dur time.Duration) RequestOption {
+ return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error {
+ r.RequestTimeout = dur
+ return nil
+ })
+}
+
+// WithEnvironmentProduction returns a RequestOption that sets the current
+// environment to be the "production" environment. An environment specifies which base URL
+// to use by default.
+func WithEnvironmentProduction() RequestOption {
+ return requestconfig.WithDefaultBaseURL("http://localhost:54321/")
+}
diff --git a/packages/tui/sdk/packages/ssestream/ssestream.go b/packages/tui/sdk/packages/ssestream/ssestream.go
new file mode 100644
index 000000000..81adbd69b
--- /dev/null
+++ b/packages/tui/sdk/packages/ssestream/ssestream.go
@@ -0,0 +1,181 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package ssestream
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "strings"
+)
+
+type Decoder interface {
+ Event() Event
+ Next() bool
+ Close() error
+ Err() error
+}
+
+func NewDecoder(res *http.Response) Decoder {
+ if res == nil || res.Body == nil {
+ return nil
+ }
+
+ var decoder Decoder
+ contentType := res.Header.Get("content-type")
+ if t, ok := decoderTypes[contentType]; ok {
+ decoder = t(res.Body)
+ } else {
+ scn := bufio.NewScanner(res.Body)
+ scn.Buffer(nil, bufio.MaxScanTokenSize<<4)
+ decoder = &eventStreamDecoder{rc: res.Body, scn: scn}
+ }
+ return decoder
+}
+
+var decoderTypes = map[string](func(io.ReadCloser) Decoder){}
+
+func RegisterDecoder(contentType string, decoder func(io.ReadCloser) Decoder) {
+ decoderTypes[strings.ToLower(contentType)] = decoder
+}
+
+type Event struct {
+ Type string
+ Data []byte
+}
+
+// A base implementation of a Decoder for text/event-stream.
+type eventStreamDecoder struct {
+ evt Event
+ rc io.ReadCloser
+ scn *bufio.Scanner
+ err error
+}
+
+func (s *eventStreamDecoder) Next() bool {
+ if s.err != nil {
+ return false
+ }
+
+ event := ""
+ data := bytes.NewBuffer(nil)
+
+ for s.scn.Scan() {
+ txt := s.scn.Bytes()
+
+ // Dispatch event on an empty line
+ if len(txt) == 0 {
+ s.evt = Event{
+ Type: event,
+ Data: data.Bytes(),
+ }
+ return true
+ }
+
+ // Split a string like "event: bar" into name="event" and value=" bar".
+ name, value, _ := bytes.Cut(txt, []byte(":"))
+
+ // Consume an optional space after the colon if it exists.
+ if len(value) > 0 && value[0] == ' ' {
+ value = value[1:]
+ }
+
+ switch string(name) {
+ case "":
+ // An empty line in the for ": something" is a comment and should be ignored.
+ continue
+ case "event":
+ event = string(value)
+ case "data":
+ _, s.err = data.Write(value)
+ if s.err != nil {
+ break
+ }
+ _, s.err = data.WriteRune('\n')
+ if s.err != nil {
+ break
+ }
+ }
+ }
+
+ if s.scn.Err() != nil {
+ s.err = s.scn.Err()
+ }
+
+ return false
+}
+
+func (s *eventStreamDecoder) Event() Event {
+ return s.evt
+}
+
+func (s *eventStreamDecoder) Close() error {
+ return s.rc.Close()
+}
+
+func (s *eventStreamDecoder) Err() error {
+ return s.err
+}
+
+type Stream[T any] struct {
+ decoder Decoder
+ cur T
+ err error
+}
+
+func NewStream[T any](decoder Decoder, err error) *Stream[T] {
+ return &Stream[T]{
+ decoder: decoder,
+ err: err,
+ }
+}
+
+// Next returns false if the stream has ended or an error occurred.
+// Call Stream.Current() to get the current value.
+// Call Stream.Err() to get the error.
+//
+// for stream.Next() {
+// data := stream.Current()
+// }
+//
+// if stream.Err() != nil {
+// ...
+// }
+func (s *Stream[T]) Next() bool {
+ if s.err != nil {
+ return false
+ }
+
+ for s.decoder.Next() {
+ var nxt T
+ s.err = json.Unmarshal(s.decoder.Event().Data, &nxt)
+ if s.err != nil {
+ return false
+ }
+ s.cur = nxt
+ return true
+ }
+
+ // decoder.Next() may be false because of an error
+ s.err = s.decoder.Err()
+
+ return false
+}
+
+func (s *Stream[T]) Current() T {
+ return s.cur
+}
+
+func (s *Stream[T]) Err() error {
+ return s.err
+}
+
+func (s *Stream[T]) Close() error {
+ if s.decoder == nil {
+ // already closed
+ return nil
+ }
+ return s.decoder.Close()
+}
diff --git a/packages/tui/sdk/release-please-config.json b/packages/tui/sdk/release-please-config.json
new file mode 100644
index 000000000..a38198eca
--- /dev/null
+++ b/packages/tui/sdk/release-please-config.json
@@ -0,0 +1,67 @@
+{
+ "packages": {
+ ".": {}
+ },
+ "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json",
+ "include-v-in-tag": true,
+ "include-component-in-tag": false,
+ "versioning": "prerelease",
+ "prerelease": true,
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": false,
+ "pull-request-header": "Automated Release PR",
+ "pull-request-title-pattern": "release: ${version}",
+ "changelog-sections": [
+ {
+ "type": "feat",
+ "section": "Features"
+ },
+ {
+ "type": "fix",
+ "section": "Bug Fixes"
+ },
+ {
+ "type": "perf",
+ "section": "Performance Improvements"
+ },
+ {
+ "type": "revert",
+ "section": "Reverts"
+ },
+ {
+ "type": "chore",
+ "section": "Chores"
+ },
+ {
+ "type": "docs",
+ "section": "Documentation"
+ },
+ {
+ "type": "style",
+ "section": "Styles"
+ },
+ {
+ "type": "refactor",
+ "section": "Refactors"
+ },
+ {
+ "type": "test",
+ "section": "Tests",
+ "hidden": true
+ },
+ {
+ "type": "build",
+ "section": "Build System"
+ },
+ {
+ "type": "ci",
+ "section": "Continuous Integration",
+ "hidden": true
+ }
+ ],
+ "release-type": "go",
+ "extra-files": [
+ "internal/version.go",
+ "README.md"
+ ]
+} \ No newline at end of file
diff --git a/packages/tui/sdk/scripts/bootstrap b/packages/tui/sdk/scripts/bootstrap
new file mode 100755
index 000000000..d6ac16540
--- /dev/null
+++ b/packages/tui/sdk/scripts/bootstrap
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ]; then
+ brew bundle check >/dev/null 2>&1 || {
+ echo "==> Installing Homebrew dependencies…"
+ brew bundle
+ }
+fi
+
+echo "==> Installing Go dependencies…"
+
+go mod tidy -e
diff --git a/packages/tui/sdk/scripts/format b/packages/tui/sdk/scripts/format
new file mode 100755
index 000000000..db2a3fa29
--- /dev/null
+++ b/packages/tui/sdk/scripts/format
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+echo "==> Running gofmt -s -w"
+gofmt -s -w .
diff --git a/packages/tui/sdk/scripts/lint b/packages/tui/sdk/scripts/lint
new file mode 100755
index 000000000..fa7ba1f6b
--- /dev/null
+++ b/packages/tui/sdk/scripts/lint
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+echo "==> Running Go build"
+go build ./...
diff --git a/packages/tui/sdk/scripts/mock b/packages/tui/sdk/scripts/mock
new file mode 100755
index 000000000..d2814ae6a
--- /dev/null
+++ b/packages/tui/sdk/scripts/mock
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+if [[ -n "$1" && "$1" != '--'* ]]; then
+ URL="$1"
+ shift
+else
+ URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)"
+fi
+
+# Check if the URL is empty
+if [ -z "$URL" ]; then
+ echo "Error: No OpenAPI spec path/url provided or found in .stats.yml"
+ exit 1
+fi
+
+echo "==> Starting mock server with URL ${URL}"
+
+# Run prism mock on the given spec
+if [ "$1" == "--daemon" ]; then
+ npm exec --package=@stainless-api/[email protected] -- prism mock "$URL" &> .prism.log &
+
+ # Wait for server to come online
+ echo -n "Waiting for server"
+ while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
+ echo -n "."
+ sleep 0.1
+ done
+
+ if grep -q "✖ fatal" ".prism.log"; then
+ cat .prism.log
+ exit 1
+ fi
+
+ echo
+else
+ npm exec --package=@stainless-api/[email protected] -- prism mock "$URL"
+fi
diff --git a/packages/tui/sdk/scripts/test b/packages/tui/sdk/scripts/test
new file mode 100755
index 000000000..efebceaee
--- /dev/null
+++ b/packages/tui/sdk/scripts/test
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[0;33m'
+NC='\033[0m' # No Color
+
+function prism_is_running() {
+ curl --silent "http://localhost:4010" >/dev/null 2>&1
+}
+
+kill_server_on_port() {
+ pids=$(lsof -t -i tcp:"$1" || echo "")
+ if [ "$pids" != "" ]; then
+ kill "$pids"
+ echo "Stopped $pids."
+ fi
+}
+
+function is_overriding_api_base_url() {
+ [ -n "$TEST_API_BASE_URL" ]
+}
+
+if ! is_overriding_api_base_url && ! prism_is_running ; then
+ # When we exit this script, make sure to kill the background mock server process
+ trap 'kill_server_on_port 4010' EXIT
+
+ # Start the dev server
+ ./scripts/mock --daemon
+fi
+
+if is_overriding_api_base_url ; then
+ echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
+ echo
+elif ! prism_is_running ; then
+ echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
+ echo -e "running against your OpenAPI spec."
+ echo
+ echo -e "To run the server, pass in the path or url of your OpenAPI"
+ echo -e "spec to the prism command:"
+ echo
+ echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}"
+ echo
+
+ exit 1
+else
+ echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
+ echo
+fi
+
+echo "==> Running tests"
+go test ./... "$@"
diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go
new file mode 100644
index 000000000..e8216a1fd
--- /dev/null
+++ b/packages/tui/sdk/session.go
@@ -0,0 +1,1385 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "reflect"
+
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+ "github.com/sst/opencode-sdk-go/internal/param"
+ "github.com/sst/opencode-sdk-go/internal/requestconfig"
+ "github.com/sst/opencode-sdk-go/option"
+ "github.com/sst/opencode-sdk-go/shared"
+ "github.com/tidwall/gjson"
+)
+
+// SessionService 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 [NewSessionService] method instead.
+type SessionService struct {
+ Options []option.RequestOption
+}
+
+// NewSessionService 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 NewSessionService(opts ...option.RequestOption) (r *SessionService) {
+ r = &SessionService{}
+ r.Options = opts
+ return
+}
+
+// Create a new session
+func (r *SessionService) New(ctx context.Context, opts ...option.RequestOption) (res *Session, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "session"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// List all sessions
+func (r *SessionService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Session, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "session"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// Delete a session and all its data
+func (r *SessionService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...)
+ return
+}
+
+// Abort a session
+func (r *SessionService) Abort(ctx context.Context, id string, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/abort", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// Create and send a new message to a session
+func (r *SessionService) Chat(ctx context.Context, id string, body SessionChatParams, opts ...option.RequestOption) (res *Message, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/message", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
+// Analyze the app and create an AGENTS.md file
+func (r *SessionService) Init(ctx context.Context, id string, body SessionInitParams, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/init", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
+// List messages for a session
+func (r *SessionService) Messages(ctx context.Context, id string, opts ...option.RequestOption) (res *[]Message, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/message", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
+// Share a session
+func (r *SessionService) Share(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/share", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+ return
+}
+
+// Summarize the session
+func (r *SessionService) Summarize(ctx context.Context, id string, body SessionSummarizeParams, opts ...option.RequestOption) (res *bool, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/summarize", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+ return
+}
+
+// Unshare the session
+func (r *SessionService) Unshare(ctx context.Context, id string, opts ...option.RequestOption) (res *Session, err error) {
+ opts = append(r.Options[:], opts...)
+ if id == "" {
+ err = errors.New("missing required id parameter")
+ return
+ }
+ path := fmt.Sprintf("session/%s/share", id)
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...)
+ return
+}
+
+type FilePart struct {
+ MediaType string `json:"mediaType,required"`
+ Type FilePartType `json:"type,required"`
+ URL string `json:"url,required"`
+ Filename string `json:"filename"`
+ JSON filePartJSON `json:"-"`
+}
+
+// filePartJSON contains the JSON metadata for the struct [FilePart]
+type filePartJSON struct {
+ MediaType apijson.Field
+ Type apijson.Field
+ URL apijson.Field
+ Filename apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *FilePart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r filePartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r FilePart) implementsMessagePart() {}
+
+type FilePartType string
+
+const (
+ FilePartTypeFile FilePartType = "file"
+)
+
+func (r FilePartType) IsKnown() bool {
+ switch r {
+ case FilePartTypeFile:
+ return true
+ }
+ return false
+}
+
+type FilePartParam struct {
+ MediaType param.Field[string] `json:"mediaType,required"`
+ Type param.Field[FilePartType] `json:"type,required"`
+ URL param.Field[string] `json:"url,required"`
+ Filename param.Field[string] `json:"filename"`
+}
+
+func (r FilePartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r FilePartParam) implementsMessagePartUnionParam() {}
+
+type Message struct {
+ ID string `json:"id,required"`
+ Metadata MessageMetadata `json:"metadata,required"`
+ Parts []MessagePart `json:"parts,required"`
+ Role MessageRole `json:"role,required"`
+ JSON messageJSON `json:"-"`
+}
+
+// messageJSON contains the JSON metadata for the struct [Message]
+type messageJSON struct {
+ ID apijson.Field
+ Metadata apijson.Field
+ Parts apijson.Field
+ Role apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Message) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadata struct {
+ SessionID string `json:"sessionID,required"`
+ Time MessageMetadataTime `json:"time,required"`
+ Tool map[string]MessageMetadataTool `json:"tool,required"`
+ Assistant MessageMetadataAssistant `json:"assistant"`
+ Error MessageMetadataError `json:"error"`
+ Snapshot string `json:"snapshot"`
+ JSON messageMetadataJSON `json:"-"`
+}
+
+// messageMetadataJSON contains the JSON metadata for the struct [MessageMetadata]
+type messageMetadataJSON struct {
+ SessionID apijson.Field
+ Time apijson.Field
+ Tool apijson.Field
+ Assistant apijson.Field
+ Error apijson.Field
+ Snapshot apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadata) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataTime struct {
+ Created float64 `json:"created,required"`
+ Completed float64 `json:"completed"`
+ JSON messageMetadataTimeJSON `json:"-"`
+}
+
+// messageMetadataTimeJSON contains the JSON metadata for the struct
+// [MessageMetadataTime]
+type messageMetadataTimeJSON struct {
+ Created apijson.Field
+ Completed apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataTime) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataTimeJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataTool struct {
+ Time MessageMetadataToolTime `json:"time,required"`
+ Title string `json:"title,required"`
+ Snapshot string `json:"snapshot"`
+ ExtraFields map[string]interface{} `json:"-,extras"`
+ JSON messageMetadataToolJSON `json:"-"`
+}
+
+// messageMetadataToolJSON contains the JSON metadata for the struct
+// [MessageMetadataTool]
+type messageMetadataToolJSON struct {
+ Time apijson.Field
+ Title apijson.Field
+ Snapshot apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataTool) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataToolJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataToolTime struct {
+ End float64 `json:"end,required"`
+ Start float64 `json:"start,required"`
+ JSON messageMetadataToolTimeJSON `json:"-"`
+}
+
+// messageMetadataToolTimeJSON contains the JSON metadata for the struct
+// [MessageMetadataToolTime]
+type messageMetadataToolTimeJSON struct {
+ End apijson.Field
+ Start apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataToolTime) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataToolTimeJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataAssistant struct {
+ Cost float64 `json:"cost,required"`
+ ModelID string `json:"modelID,required"`
+ Path MessageMetadataAssistantPath `json:"path,required"`
+ ProviderID string `json:"providerID,required"`
+ System []string `json:"system,required"`
+ Tokens MessageMetadataAssistantTokens `json:"tokens,required"`
+ Summary bool `json:"summary"`
+ JSON messageMetadataAssistantJSON `json:"-"`
+}
+
+// messageMetadataAssistantJSON contains the JSON metadata for the struct
+// [MessageMetadataAssistant]
+type messageMetadataAssistantJSON struct {
+ Cost apijson.Field
+ ModelID apijson.Field
+ Path apijson.Field
+ ProviderID apijson.Field
+ System apijson.Field
+ Tokens apijson.Field
+ Summary apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataAssistant) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataAssistantJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataAssistantPath struct {
+ Cwd string `json:"cwd,required"`
+ Root string `json:"root,required"`
+ JSON messageMetadataAssistantPathJSON `json:"-"`
+}
+
+// messageMetadataAssistantPathJSON contains the JSON metadata for the struct
+// [MessageMetadataAssistantPath]
+type messageMetadataAssistantPathJSON struct {
+ Cwd apijson.Field
+ Root apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataAssistantPath) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataAssistantPathJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataAssistantTokens struct {
+ Cache MessageMetadataAssistantTokensCache `json:"cache,required"`
+ Input float64 `json:"input,required"`
+ Output float64 `json:"output,required"`
+ Reasoning float64 `json:"reasoning,required"`
+ JSON messageMetadataAssistantTokensJSON `json:"-"`
+}
+
+// messageMetadataAssistantTokensJSON contains the JSON metadata for the struct
+// [MessageMetadataAssistantTokens]
+type messageMetadataAssistantTokensJSON struct {
+ Cache apijson.Field
+ Input apijson.Field
+ Output apijson.Field
+ Reasoning apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataAssistantTokens) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataAssistantTokensJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataAssistantTokensCache struct {
+ Read float64 `json:"read,required"`
+ Write float64 `json:"write,required"`
+ JSON messageMetadataAssistantTokensCacheJSON `json:"-"`
+}
+
+// messageMetadataAssistantTokensCacheJSON contains the JSON metadata for the
+// struct [MessageMetadataAssistantTokensCache]
+type messageMetadataAssistantTokensCacheJSON struct {
+ Read apijson.Field
+ Write apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataAssistantTokensCache) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataAssistantTokensCacheJSON) RawJSON() string {
+ return r.raw
+}
+
+type MessageMetadataError struct {
+ // This field can have the runtime type of [shared.ProviderAuthErrorData],
+ // [shared.UnknownErrorData], [interface{}].
+ Data interface{} `json:"data,required"`
+ Name MessageMetadataErrorName `json:"name,required"`
+ JSON messageMetadataErrorJSON `json:"-"`
+ union MessageMetadataErrorUnion
+}
+
+// messageMetadataErrorJSON contains the JSON metadata for the struct
+// [MessageMetadataError]
+type messageMetadataErrorJSON struct {
+ Data apijson.Field
+ Name apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r messageMetadataErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *MessageMetadataError) UnmarshalJSON(data []byte) (err error) {
+ *r = MessageMetadataError{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [MessageMetadataErrorUnion] interface which you can cast to
+// the specific types for more type safety.
+//
+// Possible runtime types of the union are [shared.ProviderAuthError],
+// [shared.UnknownError], [MessageMetadataErrorMessageOutputLengthError].
+func (r MessageMetadataError) AsUnion() MessageMetadataErrorUnion {
+ return r.union
+}
+
+// Union satisfied by [shared.ProviderAuthError], [shared.UnknownError] or
+// [MessageMetadataErrorMessageOutputLengthError].
+type MessageMetadataErrorUnion interface {
+ ImplementsMessageMetadataError()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*MessageMetadataErrorUnion)(nil)).Elem(),
+ "name",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(shared.ProviderAuthError{}),
+ DiscriminatorValue: "ProviderAuthError",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(shared.UnknownError{}),
+ DiscriminatorValue: "UnknownError",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(MessageMetadataErrorMessageOutputLengthError{}),
+ DiscriminatorValue: "MessageOutputLengthError",
+ },
+ )
+}
+
+type MessageMetadataErrorMessageOutputLengthError struct {
+ Data interface{} `json:"data,required"`
+ Name MessageMetadataErrorMessageOutputLengthErrorName `json:"name,required"`
+ JSON messageMetadataErrorMessageOutputLengthErrorJSON `json:"-"`
+}
+
+// messageMetadataErrorMessageOutputLengthErrorJSON contains the JSON metadata for
+// the struct [MessageMetadataErrorMessageOutputLengthError]
+type messageMetadataErrorMessageOutputLengthErrorJSON struct {
+ Data apijson.Field
+ Name apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *MessageMetadataErrorMessageOutputLengthError) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r messageMetadataErrorMessageOutputLengthErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r MessageMetadataErrorMessageOutputLengthError) ImplementsMessageMetadataError() {}
+
+type MessageMetadataErrorMessageOutputLengthErrorName string
+
+const (
+ MessageMetadataErrorMessageOutputLengthErrorNameMessageOutputLengthError MessageMetadataErrorMessageOutputLengthErrorName = "MessageOutputLengthError"
+)
+
+func (r MessageMetadataErrorMessageOutputLengthErrorName) IsKnown() bool {
+ switch r {
+ case MessageMetadataErrorMessageOutputLengthErrorNameMessageOutputLengthError:
+ return true
+ }
+ return false
+}
+
+type MessageMetadataErrorName string
+
+const (
+ MessageMetadataErrorNameProviderAuthError MessageMetadataErrorName = "ProviderAuthError"
+ MessageMetadataErrorNameUnknownError MessageMetadataErrorName = "UnknownError"
+ MessageMetadataErrorNameMessageOutputLengthError MessageMetadataErrorName = "MessageOutputLengthError"
+)
+
+func (r MessageMetadataErrorName) IsKnown() bool {
+ switch r {
+ case MessageMetadataErrorNameProviderAuthError, MessageMetadataErrorNameUnknownError, MessageMetadataErrorNameMessageOutputLengthError:
+ return true
+ }
+ return false
+}
+
+type MessageRole string
+
+const (
+ MessageRoleUser MessageRole = "user"
+ MessageRoleAssistant MessageRole = "assistant"
+)
+
+func (r MessageRole) IsKnown() bool {
+ switch r {
+ case MessageRoleUser, MessageRoleAssistant:
+ return true
+ }
+ return false
+}
+
+type MessagePart struct {
+ Type MessagePartType `json:"type,required"`
+ Filename string `json:"filename"`
+ MediaType string `json:"mediaType"`
+ // This field can have the runtime type of [map[string]interface{}].
+ ProviderMetadata interface{} `json:"providerMetadata"`
+ SourceID string `json:"sourceId"`
+ Text string `json:"text"`
+ Title string `json:"title"`
+ // This field can have the runtime type of [ToolInvocationPartToolInvocation].
+ ToolInvocation interface{} `json:"toolInvocation"`
+ URL string `json:"url"`
+ JSON messagePartJSON `json:"-"`
+ union MessagePartUnion
+}
+
+// messagePartJSON contains the JSON metadata for the struct [MessagePart]
+type messagePartJSON struct {
+ Type apijson.Field
+ Filename apijson.Field
+ MediaType apijson.Field
+ ProviderMetadata apijson.Field
+ SourceID apijson.Field
+ Text apijson.Field
+ Title apijson.Field
+ ToolInvocation apijson.Field
+ URL apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r messagePartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *MessagePart) UnmarshalJSON(data []byte) (err error) {
+ *r = MessagePart{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [MessagePartUnion] interface which you can cast to the
+// specific types for more type safety.
+//
+// Possible runtime types of the union are [TextPart], [ReasoningPart],
+// [ToolInvocationPart], [SourceURLPart], [FilePart], [StepStartPart].
+func (r MessagePart) AsUnion() MessagePartUnion {
+ return r.union
+}
+
+// Union satisfied by [TextPart], [ReasoningPart], [ToolInvocationPart],
+// [SourceURLPart], [FilePart] or [StepStartPart].
+type MessagePartUnion interface {
+ implementsMessagePart()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*MessagePartUnion)(nil)).Elem(),
+ "type",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(TextPart{}),
+ DiscriminatorValue: "text",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ReasoningPart{}),
+ DiscriminatorValue: "reasoning",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ToolInvocationPart{}),
+ DiscriminatorValue: "tool-invocation",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(SourceURLPart{}),
+ DiscriminatorValue: "source-url",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(FilePart{}),
+ DiscriminatorValue: "file",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(StepStartPart{}),
+ DiscriminatorValue: "step-start",
+ },
+ )
+}
+
+type MessagePartType string
+
+const (
+ MessagePartTypeText MessagePartType = "text"
+ MessagePartTypeReasoning MessagePartType = "reasoning"
+ MessagePartTypeToolInvocation MessagePartType = "tool-invocation"
+ MessagePartTypeSourceURL MessagePartType = "source-url"
+ MessagePartTypeFile MessagePartType = "file"
+ MessagePartTypeStepStart MessagePartType = "step-start"
+)
+
+func (r MessagePartType) IsKnown() bool {
+ switch r {
+ case MessagePartTypeText, MessagePartTypeReasoning, MessagePartTypeToolInvocation, MessagePartTypeSourceURL, MessagePartTypeFile, MessagePartTypeStepStart:
+ return true
+ }
+ return false
+}
+
+type MessagePartParam struct {
+ Type param.Field[MessagePartType] `json:"type,required"`
+ Filename param.Field[string] `json:"filename"`
+ MediaType param.Field[string] `json:"mediaType"`
+ ProviderMetadata param.Field[interface{}] `json:"providerMetadata"`
+ SourceID param.Field[string] `json:"sourceId"`
+ Text param.Field[string] `json:"text"`
+ Title param.Field[string] `json:"title"`
+ ToolInvocation param.Field[interface{}] `json:"toolInvocation"`
+ URL param.Field[string] `json:"url"`
+}
+
+func (r MessagePartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r MessagePartParam) implementsMessagePartUnionParam() {}
+
+// Satisfied by [TextPartParam], [ReasoningPartParam], [ToolInvocationPartParam],
+// [SourceURLPartParam], [FilePartParam], [StepStartPartParam], [MessagePartParam].
+type MessagePartUnionParam interface {
+ implementsMessagePartUnionParam()
+}
+
+type ReasoningPart struct {
+ Text string `json:"text,required"`
+ Type ReasoningPartType `json:"type,required"`
+ ProviderMetadata map[string]interface{} `json:"providerMetadata"`
+ JSON reasoningPartJSON `json:"-"`
+}
+
+// reasoningPartJSON contains the JSON metadata for the struct [ReasoningPart]
+type reasoningPartJSON struct {
+ Text apijson.Field
+ Type apijson.Field
+ ProviderMetadata apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ReasoningPart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r reasoningPartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r ReasoningPart) implementsMessagePart() {}
+
+type ReasoningPartType string
+
+const (
+ ReasoningPartTypeReasoning ReasoningPartType = "reasoning"
+)
+
+func (r ReasoningPartType) IsKnown() bool {
+ switch r {
+ case ReasoningPartTypeReasoning:
+ return true
+ }
+ return false
+}
+
+type ReasoningPartParam struct {
+ Text param.Field[string] `json:"text,required"`
+ Type param.Field[ReasoningPartType] `json:"type,required"`
+ ProviderMetadata param.Field[map[string]interface{}] `json:"providerMetadata"`
+}
+
+func (r ReasoningPartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r ReasoningPartParam) implementsMessagePartUnionParam() {}
+
+type Session struct {
+ ID string `json:"id,required"`
+ Time SessionTime `json:"time,required"`
+ Title string `json:"title,required"`
+ Version string `json:"version,required"`
+ ParentID string `json:"parentID"`
+ Revert SessionRevert `json:"revert"`
+ Share SessionShare `json:"share"`
+ JSON sessionJSON `json:"-"`
+}
+
+// sessionJSON contains the JSON metadata for the struct [Session]
+type sessionJSON struct {
+ ID apijson.Field
+ Time apijson.Field
+ Title apijson.Field
+ Version apijson.Field
+ ParentID apijson.Field
+ Revert apijson.Field
+ Share apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Session) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionJSON) RawJSON() string {
+ return r.raw
+}
+
+type SessionTime struct {
+ Created float64 `json:"created,required"`
+ Updated float64 `json:"updated,required"`
+ JSON sessionTimeJSON `json:"-"`
+}
+
+// sessionTimeJSON contains the JSON metadata for the struct [SessionTime]
+type sessionTimeJSON struct {
+ Created apijson.Field
+ Updated apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *SessionTime) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionTimeJSON) RawJSON() string {
+ return r.raw
+}
+
+type SessionRevert struct {
+ MessageID string `json:"messageID,required"`
+ Part float64 `json:"part,required"`
+ Snapshot string `json:"snapshot"`
+ JSON sessionRevertJSON `json:"-"`
+}
+
+// sessionRevertJSON contains the JSON metadata for the struct [SessionRevert]
+type sessionRevertJSON struct {
+ MessageID apijson.Field
+ Part apijson.Field
+ Snapshot apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *SessionRevert) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionRevertJSON) RawJSON() string {
+ return r.raw
+}
+
+type SessionShare struct {
+ URL string `json:"url,required"`
+ JSON sessionShareJSON `json:"-"`
+}
+
+// sessionShareJSON contains the JSON metadata for the struct [SessionShare]
+type sessionShareJSON struct {
+ URL apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *SessionShare) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sessionShareJSON) RawJSON() string {
+ return r.raw
+}
+
+type SourceURLPart struct {
+ SourceID string `json:"sourceId,required"`
+ Type SourceURLPartType `json:"type,required"`
+ URL string `json:"url,required"`
+ ProviderMetadata map[string]interface{} `json:"providerMetadata"`
+ Title string `json:"title"`
+ JSON sourceURLPartJSON `json:"-"`
+}
+
+// sourceURLPartJSON contains the JSON metadata for the struct [SourceURLPart]
+type sourceURLPartJSON struct {
+ SourceID apijson.Field
+ Type apijson.Field
+ URL apijson.Field
+ ProviderMetadata apijson.Field
+ Title apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *SourceURLPart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r sourceURLPartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r SourceURLPart) implementsMessagePart() {}
+
+type SourceURLPartType string
+
+const (
+ SourceURLPartTypeSourceURL SourceURLPartType = "source-url"
+)
+
+func (r SourceURLPartType) IsKnown() bool {
+ switch r {
+ case SourceURLPartTypeSourceURL:
+ return true
+ }
+ return false
+}
+
+type SourceURLPartParam struct {
+ SourceID param.Field[string] `json:"sourceId,required"`
+ Type param.Field[SourceURLPartType] `json:"type,required"`
+ URL param.Field[string] `json:"url,required"`
+ ProviderMetadata param.Field[map[string]interface{}] `json:"providerMetadata"`
+ Title param.Field[string] `json:"title"`
+}
+
+func (r SourceURLPartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r SourceURLPartParam) implementsMessagePartUnionParam() {}
+
+type StepStartPart struct {
+ Type StepStartPartType `json:"type,required"`
+ JSON stepStartPartJSON `json:"-"`
+}
+
+// stepStartPartJSON contains the JSON metadata for the struct [StepStartPart]
+type stepStartPartJSON struct {
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *StepStartPart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r stepStartPartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r StepStartPart) implementsMessagePart() {}
+
+type StepStartPartType string
+
+const (
+ StepStartPartTypeStepStart StepStartPartType = "step-start"
+)
+
+func (r StepStartPartType) IsKnown() bool {
+ switch r {
+ case StepStartPartTypeStepStart:
+ return true
+ }
+ return false
+}
+
+type StepStartPartParam struct {
+ Type param.Field[StepStartPartType] `json:"type,required"`
+}
+
+func (r StepStartPartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r StepStartPartParam) implementsMessagePartUnionParam() {}
+
+type TextPart struct {
+ Text string `json:"text,required"`
+ Type TextPartType `json:"type,required"`
+ JSON textPartJSON `json:"-"`
+}
+
+// textPartJSON contains the JSON metadata for the struct [TextPart]
+type textPartJSON struct {
+ Text apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *TextPart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r textPartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r TextPart) implementsMessagePart() {}
+
+type TextPartType string
+
+const (
+ TextPartTypeText TextPartType = "text"
+)
+
+func (r TextPartType) IsKnown() bool {
+ switch r {
+ case TextPartTypeText:
+ return true
+ }
+ return false
+}
+
+type TextPartParam struct {
+ Text param.Field[string] `json:"text,required"`
+ Type param.Field[TextPartType] `json:"type,required"`
+}
+
+func (r TextPartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r TextPartParam) implementsMessagePartUnionParam() {}
+
+type ToolCall struct {
+ State ToolCallState `json:"state,required"`
+ ToolCallID string `json:"toolCallId,required"`
+ ToolName string `json:"toolName,required"`
+ Args interface{} `json:"args"`
+ Step float64 `json:"step"`
+ JSON toolCallJSON `json:"-"`
+}
+
+// toolCallJSON contains the JSON metadata for the struct [ToolCall]
+type toolCallJSON struct {
+ State apijson.Field
+ ToolCallID apijson.Field
+ ToolName apijson.Field
+ Args apijson.Field
+ Step apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ToolCall) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r toolCallJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r ToolCall) implementsToolInvocationPartToolInvocation() {}
+
+type ToolCallState string
+
+const (
+ ToolCallStateCall ToolCallState = "call"
+)
+
+func (r ToolCallState) IsKnown() bool {
+ switch r {
+ case ToolCallStateCall:
+ return true
+ }
+ return false
+}
+
+type ToolCallParam struct {
+ State param.Field[ToolCallState] `json:"state,required"`
+ ToolCallID param.Field[string] `json:"toolCallId,required"`
+ ToolName param.Field[string] `json:"toolName,required"`
+ Args param.Field[interface{}] `json:"args"`
+ Step param.Field[float64] `json:"step"`
+}
+
+func (r ToolCallParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r ToolCallParam) implementsToolInvocationPartToolInvocationUnionParam() {}
+
+type ToolInvocationPart struct {
+ ToolInvocation ToolInvocationPartToolInvocation `json:"toolInvocation,required"`
+ Type ToolInvocationPartType `json:"type,required"`
+ JSON toolInvocationPartJSON `json:"-"`
+}
+
+// toolInvocationPartJSON contains the JSON metadata for the struct
+// [ToolInvocationPart]
+type toolInvocationPartJSON struct {
+ ToolInvocation apijson.Field
+ Type apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ToolInvocationPart) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r toolInvocationPartJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r ToolInvocationPart) implementsMessagePart() {}
+
+type ToolInvocationPartToolInvocation struct {
+ State ToolInvocationPartToolInvocationState `json:"state,required"`
+ ToolCallID string `json:"toolCallId,required"`
+ ToolName string `json:"toolName,required"`
+ // This field can have the runtime type of [interface{}].
+ Args interface{} `json:"args"`
+ Result string `json:"result"`
+ Step float64 `json:"step"`
+ JSON toolInvocationPartToolInvocationJSON `json:"-"`
+ union ToolInvocationPartToolInvocationUnion
+}
+
+// toolInvocationPartToolInvocationJSON contains the JSON metadata for the struct
+// [ToolInvocationPartToolInvocation]
+type toolInvocationPartToolInvocationJSON struct {
+ State apijson.Field
+ ToolCallID apijson.Field
+ ToolName apijson.Field
+ Args apijson.Field
+ Result apijson.Field
+ Step apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r toolInvocationPartToolInvocationJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r *ToolInvocationPartToolInvocation) UnmarshalJSON(data []byte) (err error) {
+ *r = ToolInvocationPartToolInvocation{}
+ err = apijson.UnmarshalRoot(data, &r.union)
+ if err != nil {
+ return err
+ }
+ return apijson.Port(r.union, &r)
+}
+
+// AsUnion returns a [ToolInvocationPartToolInvocationUnion] interface which you
+// can cast to the specific types for more type safety.
+//
+// Possible runtime types of the union are [ToolCall], [ToolPartialCall],
+// [ToolResult].
+func (r ToolInvocationPartToolInvocation) AsUnion() ToolInvocationPartToolInvocationUnion {
+ return r.union
+}
+
+// Union satisfied by [ToolCall], [ToolPartialCall] or [ToolResult].
+type ToolInvocationPartToolInvocationUnion interface {
+ implementsToolInvocationPartToolInvocation()
+}
+
+func init() {
+ apijson.RegisterUnion(
+ reflect.TypeOf((*ToolInvocationPartToolInvocationUnion)(nil)).Elem(),
+ "state",
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ToolCall{}),
+ DiscriminatorValue: "call",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ToolPartialCall{}),
+ DiscriminatorValue: "partial-call",
+ },
+ apijson.UnionVariant{
+ TypeFilter: gjson.JSON,
+ Type: reflect.TypeOf(ToolResult{}),
+ DiscriminatorValue: "result",
+ },
+ )
+}
+
+type ToolInvocationPartToolInvocationState string
+
+const (
+ ToolInvocationPartToolInvocationStateCall ToolInvocationPartToolInvocationState = "call"
+ ToolInvocationPartToolInvocationStatePartialCall ToolInvocationPartToolInvocationState = "partial-call"
+ ToolInvocationPartToolInvocationStateResult ToolInvocationPartToolInvocationState = "result"
+)
+
+func (r ToolInvocationPartToolInvocationState) IsKnown() bool {
+ switch r {
+ case ToolInvocationPartToolInvocationStateCall, ToolInvocationPartToolInvocationStatePartialCall, ToolInvocationPartToolInvocationStateResult:
+ return true
+ }
+ return false
+}
+
+type ToolInvocationPartType string
+
+const (
+ ToolInvocationPartTypeToolInvocation ToolInvocationPartType = "tool-invocation"
+)
+
+func (r ToolInvocationPartType) IsKnown() bool {
+ switch r {
+ case ToolInvocationPartTypeToolInvocation:
+ return true
+ }
+ return false
+}
+
+type ToolInvocationPartParam struct {
+ ToolInvocation param.Field[ToolInvocationPartToolInvocationUnionParam] `json:"toolInvocation,required"`
+ Type param.Field[ToolInvocationPartType] `json:"type,required"`
+}
+
+func (r ToolInvocationPartParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r ToolInvocationPartParam) implementsMessagePartUnionParam() {}
+
+type ToolInvocationPartToolInvocationParam struct {
+ State param.Field[ToolInvocationPartToolInvocationState] `json:"state,required"`
+ ToolCallID param.Field[string] `json:"toolCallId,required"`
+ ToolName param.Field[string] `json:"toolName,required"`
+ Args param.Field[interface{}] `json:"args"`
+ Result param.Field[string] `json:"result"`
+ Step param.Field[float64] `json:"step"`
+}
+
+func (r ToolInvocationPartToolInvocationParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r ToolInvocationPartToolInvocationParam) implementsToolInvocationPartToolInvocationUnionParam() {
+}
+
+// Satisfied by [ToolCallParam], [ToolPartialCallParam], [ToolResultParam],
+// [ToolInvocationPartToolInvocationParam].
+type ToolInvocationPartToolInvocationUnionParam interface {
+ implementsToolInvocationPartToolInvocationUnionParam()
+}
+
+type ToolPartialCall struct {
+ State ToolPartialCallState `json:"state,required"`
+ ToolCallID string `json:"toolCallId,required"`
+ ToolName string `json:"toolName,required"`
+ Args interface{} `json:"args"`
+ Step float64 `json:"step"`
+ JSON toolPartialCallJSON `json:"-"`
+}
+
+// toolPartialCallJSON contains the JSON metadata for the struct [ToolPartialCall]
+type toolPartialCallJSON struct {
+ State apijson.Field
+ ToolCallID apijson.Field
+ ToolName apijson.Field
+ Args apijson.Field
+ Step apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ToolPartialCall) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r toolPartialCallJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r ToolPartialCall) implementsToolInvocationPartToolInvocation() {}
+
+type ToolPartialCallState string
+
+const (
+ ToolPartialCallStatePartialCall ToolPartialCallState = "partial-call"
+)
+
+func (r ToolPartialCallState) IsKnown() bool {
+ switch r {
+ case ToolPartialCallStatePartialCall:
+ return true
+ }
+ return false
+}
+
+type ToolPartialCallParam struct {
+ State param.Field[ToolPartialCallState] `json:"state,required"`
+ ToolCallID param.Field[string] `json:"toolCallId,required"`
+ ToolName param.Field[string] `json:"toolName,required"`
+ Args param.Field[interface{}] `json:"args"`
+ Step param.Field[float64] `json:"step"`
+}
+
+func (r ToolPartialCallParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r ToolPartialCallParam) implementsToolInvocationPartToolInvocationUnionParam() {}
+
+type ToolResult struct {
+ Result string `json:"result,required"`
+ State ToolResultState `json:"state,required"`
+ ToolCallID string `json:"toolCallId,required"`
+ ToolName string `json:"toolName,required"`
+ Args interface{} `json:"args"`
+ Step float64 `json:"step"`
+ JSON toolResultJSON `json:"-"`
+}
+
+// toolResultJSON contains the JSON metadata for the struct [ToolResult]
+type toolResultJSON struct {
+ Result apijson.Field
+ State apijson.Field
+ ToolCallID apijson.Field
+ ToolName apijson.Field
+ Args apijson.Field
+ Step apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ToolResult) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r toolResultJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r ToolResult) implementsToolInvocationPartToolInvocation() {}
+
+type ToolResultState string
+
+const (
+ ToolResultStateResult ToolResultState = "result"
+)
+
+func (r ToolResultState) IsKnown() bool {
+ switch r {
+ case ToolResultStateResult:
+ return true
+ }
+ return false
+}
+
+type ToolResultParam struct {
+ Result param.Field[string] `json:"result,required"`
+ State param.Field[ToolResultState] `json:"state,required"`
+ ToolCallID param.Field[string] `json:"toolCallId,required"`
+ ToolName param.Field[string] `json:"toolName,required"`
+ Args param.Field[interface{}] `json:"args"`
+ Step param.Field[float64] `json:"step"`
+}
+
+func (r ToolResultParam) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+func (r ToolResultParam) implementsToolInvocationPartToolInvocationUnionParam() {}
+
+type SessionChatParams struct {
+ ModelID param.Field[string] `json:"modelID,required"`
+ Parts param.Field[[]MessagePartUnionParam] `json:"parts,required"`
+ ProviderID param.Field[string] `json:"providerID,required"`
+}
+
+func (r SessionChatParams) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+type SessionInitParams struct {
+ ModelID param.Field[string] `json:"modelID,required"`
+ ProviderID param.Field[string] `json:"providerID,required"`
+}
+
+func (r SessionInitParams) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
+
+type SessionSummarizeParams struct {
+ ModelID param.Field[string] `json:"modelID,required"`
+ ProviderID param.Field[string] `json:"providerID,required"`
+}
+
+func (r SessionSummarizeParams) MarshalJSON() (data []byte, err error) {
+ return apijson.MarshalRoot(r)
+}
diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go
new file mode 100644
index 000000000..da9fb8252
--- /dev/null
+++ b/packages/tui/sdk/session_test.go
@@ -0,0 +1,259 @@
+// 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 TestSessionNew(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.New(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 TestSessionList(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.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())
+ }
+}
+
+func TestSessionDelete(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.Delete(context.TODO(), "id")
+ 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 TestSessionAbort(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.Abort(context.TODO(), "id")
+ 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 TestSessionChat(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.Chat(
+ context.TODO(),
+ "id",
+ opencode.SessionChatParams{
+ ModelID: opencode.F("modelID"),
+ Parts: opencode.F([]opencode.MessagePartUnionParam{opencode.TextPartParam{
+ Text: opencode.F("text"),
+ Type: opencode.F(opencode.TextPartTypeText),
+ }}),
+ ProviderID: opencode.F("providerID"),
+ },
+ )
+ 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 TestSessionInit(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.Init(
+ context.TODO(),
+ "id",
+ opencode.SessionInitParams{
+ ModelID: opencode.F("modelID"),
+ ProviderID: opencode.F("providerID"),
+ },
+ )
+ 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 TestSessionMessages(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.Messages(context.TODO(), "id")
+ 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 TestSessionShare(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.Share(context.TODO(), "id")
+ 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 TestSessionSummarize(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.Summarize(
+ context.TODO(),
+ "id",
+ opencode.SessionSummarizeParams{
+ ModelID: opencode.F("modelID"),
+ ProviderID: opencode.F("providerID"),
+ },
+ )
+ 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 TestSessionUnshare(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.Unshare(context.TODO(), "id")
+ 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/sdk/shared/shared.go b/packages/tui/sdk/shared/shared.go
new file mode 100644
index 000000000..121f64a5a
--- /dev/null
+++ b/packages/tui/sdk/shared/shared.go
@@ -0,0 +1,132 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package shared
+
+import (
+ "github.com/sst/opencode-sdk-go/internal/apijson"
+)
+
+type ProviderAuthError struct {
+ Data ProviderAuthErrorData `json:"data,required"`
+ Name ProviderAuthErrorName `json:"name,required"`
+ JSON providerAuthErrorJSON `json:"-"`
+}
+
+// providerAuthErrorJSON contains the JSON metadata for the struct
+// [ProviderAuthError]
+type providerAuthErrorJSON struct {
+ Data apijson.Field
+ Name apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ProviderAuthError) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r providerAuthErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r ProviderAuthError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
+
+func (r ProviderAuthError) ImplementsMessageMetadataError() {}
+
+type ProviderAuthErrorData struct {
+ Message string `json:"message,required"`
+ ProviderID string `json:"providerID,required"`
+ JSON providerAuthErrorDataJSON `json:"-"`
+}
+
+// providerAuthErrorDataJSON contains the JSON metadata for the struct
+// [ProviderAuthErrorData]
+type providerAuthErrorDataJSON struct {
+ Message apijson.Field
+ ProviderID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ProviderAuthErrorData) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r providerAuthErrorDataJSON) RawJSON() string {
+ return r.raw
+}
+
+type ProviderAuthErrorName string
+
+const (
+ ProviderAuthErrorNameProviderAuthError ProviderAuthErrorName = "ProviderAuthError"
+)
+
+func (r ProviderAuthErrorName) IsKnown() bool {
+ switch r {
+ case ProviderAuthErrorNameProviderAuthError:
+ return true
+ }
+ return false
+}
+
+type UnknownError struct {
+ Data UnknownErrorData `json:"data,required"`
+ Name UnknownErrorName `json:"name,required"`
+ JSON unknownErrorJSON `json:"-"`
+}
+
+// unknownErrorJSON contains the JSON metadata for the struct [UnknownError]
+type unknownErrorJSON struct {
+ Data apijson.Field
+ Name apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *UnknownError) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r unknownErrorJSON) RawJSON() string {
+ return r.raw
+}
+
+func (r UnknownError) ImplementsEventListResponseEventSessionErrorPropertiesError() {}
+
+func (r UnknownError) ImplementsMessageMetadataError() {}
+
+type UnknownErrorData struct {
+ Message string `json:"message,required"`
+ JSON unknownErrorDataJSON `json:"-"`
+}
+
+// unknownErrorDataJSON contains the JSON metadata for the struct
+// [UnknownErrorData]
+type unknownErrorDataJSON struct {
+ Message apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *UnknownErrorData) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r unknownErrorDataJSON) RawJSON() string {
+ return r.raw
+}
+
+type UnknownErrorName string
+
+const (
+ UnknownErrorNameUnknownError UnknownErrorName = "UnknownError"
+)
+
+func (r UnknownErrorName) IsKnown() bool {
+ switch r {
+ case UnknownErrorNameUnknownError:
+ return true
+ }
+ return false
+}
diff --git a/packages/tui/sdk/usage_test.go b/packages/tui/sdk/usage_test.go
new file mode 100644
index 000000000..0e261a7aa
--- /dev/null
+++ b/packages/tui/sdk/usage_test.go
@@ -0,0 +1,32 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+package opencode_test
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/sst/opencode-sdk-go"
+ "github.com/sst/opencode-sdk-go/internal/testutil"
+ "github.com/sst/opencode-sdk-go/option"
+)
+
+func TestUsage(t *testing.T) {
+ 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),
+ )
+ events, err := client.Event.List(context.TODO())
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ t.Logf("%+v\n", events)
+}
diff --git a/scripts/stainless b/scripts/stainless
new file mode 100755
index 000000000..dae9016f3
--- /dev/null
+++ b/scripts/stainless
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -e
+
+echo "Starting opencode server on port 4096..."
+bun run ./packages/opencode/src/index.ts serve --port 4096 &
+SERVER_PID=$!
+
+echo "Waiting for server to start..."
+sleep 3
+
+echo "Fetching OpenAPI spec from http://localhost:4096/doc..."
+curl -s http://localhost:4096/doc > openapi.json
+
+echo "Stopping server..."
+kill $SERVER_PID
+
+echo "Running stl builds create..."
+stl builds create --branch dev --pull --allow-empty --targets go
+
+echo "Cleaning up..."
+rm -rf packages/tui/sdk
+mv opencode-go/ packages/tui/sdk/
+rm -rf packages/tui/sdk/.git
+
+echo "Done!"
diff --git a/stainless-workspace.json b/stainless-workspace.json
new file mode 100644
index 000000000..b4230b05e
--- /dev/null
+++ b/stainless-workspace.json
@@ -0,0 +1,5 @@
+{
+ "project": "opencode",
+ "openapi_spec": "openapi.json",
+ "stainless_config": "stainless.yml"
+}