Просмотр исходного кода

chore: better local dev with stainless script

adamdottv 9 месяцев назад
Родитель
Сommit
5a0910ea79
70 измененных файлов с 11281 добавлено и 2 удалено
  1. 1 0
      .gitignore
  2. 1 0
      package.json
  3. 2 0
      packages/tui/go.mod
  4. 0 2
      packages/tui/go.sum
  5. 7 0
      packages/tui/sdk/.devcontainer/devcontainer.json
  6. 49 0
      packages/tui/sdk/.github/workflows/ci.yml
  7. 4 0
      packages/tui/sdk/.gitignore
  8. 3 0
      packages/tui/sdk/.release-please-manifest.json
  9. 4 0
      packages/tui/sdk/.stats.yml
  10. 1 0
      packages/tui/sdk/Brewfile
  11. 73 0
      packages/tui/sdk/CHANGELOG.md
  12. 66 0
      packages/tui/sdk/CONTRIBUTING.md
  13. 201 0
      packages/tui/sdk/LICENSE
  14. 354 0
      packages/tui/sdk/README.md
  15. 27 0
      packages/tui/sdk/SECURITY.md
  16. 34 0
      packages/tui/sdk/aliases.go
  17. 110 0
      packages/tui/sdk/api.md
  18. 123 0
      packages/tui/sdk/app.go
  19. 58 0
      packages/tui/sdk/app_test.go
  20. 123 0
      packages/tui/sdk/client.go
  21. 332 0
      packages/tui/sdk/client_test.go
  22. 724 0
      packages/tui/sdk/config.go
  23. 58 0
      packages/tui/sdk/config_test.go
  24. 1180 0
      packages/tui/sdk/event.go
  25. 4 0
      packages/tui/sdk/examples/.keep
  26. 50 0
      packages/tui/sdk/field.go
  27. 143 0
      packages/tui/sdk/file.go
  28. 60 0
      packages/tui/sdk/file_test.go
  29. 213 0
      packages/tui/sdk/find.go
  30. 86 0
      packages/tui/sdk/find_test.go
  31. 13 0
      packages/tui/sdk/go.mod
  32. 10 0
      packages/tui/sdk/go.sum
  33. 53 0
      packages/tui/sdk/internal/apierror/apierror.go
  34. 383 0
      packages/tui/sdk/internal/apiform/encoder.go
  35. 5 0
      packages/tui/sdk/internal/apiform/form.go
  36. 440 0
      packages/tui/sdk/internal/apiform/form_test.go
  37. 48 0
      packages/tui/sdk/internal/apiform/tag.go
  38. 670 0
      packages/tui/sdk/internal/apijson/decoder.go
  39. 398 0
      packages/tui/sdk/internal/apijson/encoder.go
  40. 41 0
      packages/tui/sdk/internal/apijson/field.go
  41. 66 0
      packages/tui/sdk/internal/apijson/field_test.go
  42. 617 0
      packages/tui/sdk/internal/apijson/json_test.go
  43. 120 0
      packages/tui/sdk/internal/apijson/port.go
  44. 257 0
      packages/tui/sdk/internal/apijson/port_test.go
  45. 41 0
      packages/tui/sdk/internal/apijson/registry.go
  46. 47 0
      packages/tui/sdk/internal/apijson/tag.go
  47. 341 0
      packages/tui/sdk/internal/apiquery/encoder.go
  48. 50 0
      packages/tui/sdk/internal/apiquery/query.go
  49. 335 0
      packages/tui/sdk/internal/apiquery/query_test.go
  50. 41 0
      packages/tui/sdk/internal/apiquery/tag.go
  51. 29 0
      packages/tui/sdk/internal/param/field.go
  52. 629 0
      packages/tui/sdk/internal/requestconfig/requestconfig.go
  53. 27 0
      packages/tui/sdk/internal/testutil/testutil.go
  54. 5 0
      packages/tui/sdk/internal/version.go
  55. 4 0
      packages/tui/sdk/lib/.keep
  56. 38 0
      packages/tui/sdk/option/middleware.go
  57. 266 0
      packages/tui/sdk/option/requestoption.go
  58. 181 0
      packages/tui/sdk/packages/ssestream/ssestream.go
  59. 67 0
      packages/tui/sdk/release-please-config.json
  60. 16 0
      packages/tui/sdk/scripts/bootstrap
  61. 8 0
      packages/tui/sdk/scripts/format
  62. 8 0
      packages/tui/sdk/scripts/lint
  63. 41 0
      packages/tui/sdk/scripts/mock
  64. 56 0
      packages/tui/sdk/scripts/test
  65. 1385 0
      packages/tui/sdk/session.go
  66. 259 0
      packages/tui/sdk/session_test.go
  67. 132 0
      packages/tui/sdk/shared/shared.go
  68. 32 0
      packages/tui/sdk/usage_test.go
  69. 26 0
      scripts/stainless
  70. 5 0
      stainless-workspace.json

+ 1 - 0
.gitignore

@@ -5,3 +5,4 @@ node_modules
 .env
 .idea
 .vscode
+openapi.json

+ 1 - 0
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": {

+ 2 - 0
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 (

+ 0 - 2
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=

+ 7 - 0
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"
+}

+ 49 - 0
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

+ 4 - 0
packages/tui/sdk/.gitignore

@@ -0,0 +1,4 @@
+.prism.log
+codegen.log
+Brewfile.lock.json
+.idea/

+ 3 - 0
packages/tui/sdk/.release-please-manifest.json

@@ -0,0 +1,3 @@
+{
+  ".": "0.1.0-alpha.8"
+}

+ 4 - 0
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

+ 1 - 0
packages/tui/sdk/Brewfile

@@ -0,0 +1 @@
+brew "go"

+ 73 - 0
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))

+ 66 - 0
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
+```

+ 201 - 0
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.

+ 354 - 0
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).

+ 27 - 0
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.

+ 34 - 0
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

+ 110 - 0
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>

+ 123 - 0
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
+}

+ 58 - 0
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())
+	}
+}

+ 123 - 0
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...)
+}

+ 332 - 0
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 }

+ 724 - 0
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
+}

+ 58 - 0
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())
+	}
+}

+ 1180 - 0
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
+}

+ 4 - 0
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.

+ 50 - 0
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 }

+ 143 - 0
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,
+	})
+}

+ 60 - 0
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())
+	}
+}

+ 213 - 0
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,
+	})
+}

+ 86 - 0
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())
+	}
+}

+ 13 - 0
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
+)

+ 10 - 0
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=

+ 53 - 0
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
+}

+ 383 - 0
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)
+	}
+}

+ 5 - 0
packages/tui/sdk/internal/apiform/form.go

@@ -0,0 +1,5 @@
+package apiform
+
+type Marshaler interface {
+	MarshalMultipart() ([]byte, string, error)
+}

+ 440 - 0
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))
+			}
+		})
+	}
+}

+ 48 - 0
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
+}

+ 670 - 0
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
+}

+ 398 - 0
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(".", "\\.", ":", "\\:", "*", "\\*")

+ 41 - 0
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
+}

+ 66 - 0
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))
+			}
+		})
+	}
+}

+ 617 - 0
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))
+			}
+		})
+	}
+}

+ 120 - 0
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
+}

+ 257 - 0
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())
+			}
+		})
+	}
+}

+ 41 - 0
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)
+}

+ 47 - 0
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
+}

+ 341 - 0
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)
+	}
+
+}

+ 50 - 0
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
+)

+ 335 - 0
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)
+			}
+		})
+	}
+}

+ 41 - 0
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
+}

+ 29 - 0
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)
+}

+ 629 - 0
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
+	})
+}

+ 27 - 0
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
+}

+ 5 - 0
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

+ 4 - 0
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.

+ 38 - 0
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
+	})
+}

+ 266 - 0
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/")
+}

+ 181 - 0
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()
+}

+ 67 - 0
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"
+  ]
+}

+ 16 - 0
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

+ 8 - 0
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 .

+ 8 - 0
packages/tui/sdk/scripts/lint

@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+echo "==> Running Go build"
+go build ./...

+ 41 - 0
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

+ 56 - 0
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 ./... "$@"

+ 1385 - 0
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)
+}

+ 259 - 0
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())
+	}
+}

+ 132 - 0
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
+}

+ 32 - 0
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)
+}

+ 26 - 0
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!"

+ 5 - 0
stainless-workspace.json

@@ -0,0 +1,5 @@
+{
+  "project": "opencode",
+  "openapi_spec": "openapi.json",
+  "stainless_config": "stainless.yml"
+}