Dax Raad 7 месяцев назад
Родитель
Сommit
f20ef61bc7

+ 0 - 2
packages/opencode/src/cli/cmd/serve.ts

@@ -1,6 +1,5 @@
 import { Provider } from "../../provider/provider"
 import { Server } from "../../server/server"
-import { Share } from "../../share/share"
 import { bootstrap } from "../bootstrap"
 import { cmd } from "./cmd"
 
@@ -32,7 +31,6 @@ export const ServeCommand = cmd({
       const hostname = args.hostname
       const port = args.port
 
-      await Share.init()
       const server = Server.listen({
         port,
         hostname,

+ 13 - 2
packages/opencode/src/cli/cmd/tui.ts

@@ -36,6 +36,17 @@ export const TuiCommand = cmd({
       .option("mode", {
         type: "string",
         describe: "mode to use",
+      })
+      .option("port", {
+        type: "number",
+        describe: "port to listen on",
+        default: 0,
+      })
+      .option("hostname", {
+        alias: ["h"],
+        type: "string",
+        describe: "hostname to listen on",
+        default: "127.0.0.1",
       }),
   handler: async (args) => {
     while (true) {
@@ -54,8 +65,8 @@ export const TuiCommand = cmd({
         }
 
         const server = Server.listen({
-          port: 0,
-          hostname: "127.0.0.1",
+          port: args.port,
+          hostname: args.hostname,
         })
 
         let cmd = ["go", "run", "./main.go"]

+ 43 - 0
packages/opencode/src/server/server.ts

@@ -17,6 +17,7 @@ import { File } from "../file"
 import { LSP } from "../lsp"
 import { MessageV2 } from "../session/message-v2"
 import { Mode } from "../session/mode"
+import { callTui, TuiRoute } from "./tui"
 
 const ERRORS = {
   400: {
@@ -703,6 +704,48 @@ export namespace Server {
           return c.json(modes)
         },
       )
+      .post(
+        "/tui/prompt",
+        describeRoute({
+          description: "Send a prompt to the TUI",
+          responses: {
+            200: {
+              description: "Prompt processed successfully",
+              content: {
+                "application/json": {
+                  schema: resolver(z.boolean()),
+                },
+              },
+            },
+          },
+        }),
+        zValidator(
+          "json",
+          z.object({
+            text: z.string(),
+            parts: MessageV2.Part.array(),
+          }),
+        ),
+        async (c) => c.json(await callTui(c)),
+      )
+      .post(
+        "/tui/open-help",
+        describeRoute({
+          description: "Open the help dialog",
+          responses: {
+            200: {
+              description: "Help dialog opened successfully",
+              content: {
+                "application/json": {
+                  schema: resolver(z.boolean()),
+                },
+              },
+            },
+          },
+        }),
+        async (c) => c.json(await callTui(c)),
+      )
+      .route("/tui/control", TuiRoute)
 
     return result
   }

+ 30 - 0
packages/opencode/src/server/tui.ts

@@ -0,0 +1,30 @@
+import { Hono, type Context } from "hono"
+import { AsyncQueue } from "../util/queue"
+
+interface Request {
+  path: string
+  body: any
+}
+
+const request = new AsyncQueue<Request>()
+const response = new AsyncQueue<any>()
+
+export async function callTui(ctx: Context) {
+  const body = await ctx.req.json()
+  request.push({
+    path: ctx.req.path,
+    body,
+  })
+  return response.next()
+}
+
+export const TuiRoute = new Hono()
+  .get("/next", async (c) => {
+    const req = await request.next()
+    return c.json(req)
+  })
+  .post("/response", async (c) => {
+    const body = await c.req.json()
+    response.push(body)
+    return c.json(true)
+  })

+ 19 - 0
packages/opencode/src/util/queue.ts

@@ -0,0 +1,19 @@
+export class AsyncQueue<T> implements AsyncIterable<T> {
+  private queue: T[] = []
+  private resolvers: ((value: T) => void)[] = []
+
+  push(item: T) {
+    const resolve = this.resolvers.shift()
+    if (resolve) resolve(item)
+    else this.queue.push(item)
+  }
+
+  async next(): Promise<T> {
+    if (this.queue.length > 0) return this.queue.shift()!
+    return new Promise((resolve) => this.resolvers.push(resolve))
+  }
+
+  async *[Symbol.asyncIterator]() {
+    while (true) yield await this.next()
+  }
+}

+ 3 - 0
packages/tui/cmd/opencode/main.go

@@ -13,6 +13,7 @@ import (
 	flag "github.com/spf13/pflag"
 	"github.com/sst/opencode-sdk-go"
 	"github.com/sst/opencode-sdk-go/option"
+	"github.com/sst/opencode/internal/api"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/clipboard"
 	"github.com/sst/opencode/internal/tui"
@@ -100,6 +101,8 @@ func main() {
 		}
 	}()
 
+	go api.Start(ctx, program, httpClient)
+
 	// Handle signals in a separate goroutine
 	go func() {
 		sig := <-sigChan

+ 41 - 0
packages/tui/internal/api/api.go

@@ -0,0 +1,41 @@
+package api
+
+import (
+	"context"
+	"encoding/json"
+	"log"
+
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/sst/opencode-sdk-go"
+)
+
+type Request struct {
+	Path string          `json:"path"`
+	Body json.RawMessage `json:"body"`
+}
+
+func Start(ctx context.Context, program *tea.Program, client *opencode.Client) {
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		default:
+			var req Request
+			if err := client.Get(ctx, "/tui/control/next", nil, &req); err != nil {
+				log.Printf("Error getting next request: %v", err)
+				continue
+			}
+			program.Send(req)
+		}
+	}
+}
+
+func Reply(ctx context.Context, client *opencode.Client, response interface{}) tea.Cmd {
+	return func() tea.Msg {
+		err := client.Post(ctx, "/tui/control/response", response, nil)
+		if err != nil {
+			return err
+		}
+		return nil
+	}
+}

+ 31 - 9
packages/tui/internal/tui/tui.go

@@ -2,6 +2,7 @@ package tui
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"log/slog"
 	"os"
@@ -15,6 +16,7 @@ import (
 	"github.com/charmbracelet/lipgloss/v2"
 
 	"github.com/sst/opencode-sdk-go"
+	"github.com/sst/opencode/internal/api"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/commands"
 	"github.com/sst/opencode/internal/completions"
@@ -57,7 +59,7 @@ const (
 const interruptDebounceTimeout = 1 * time.Second
 const exitDebounceTimeout = 1 * time.Second
 
-type appModel struct {
+type Model struct {
 	width, height        int
 	app                  *app.App
 	modal                layout.Modal
@@ -78,7 +80,7 @@ type appModel struct {
 	fileViewer        fileviewer.Model
 }
 
-func (a appModel) Init() tea.Cmd {
+func (a Model) Init() tea.Cmd {
 	var cmds []tea.Cmd
 	// https://github.com/charmbracelet/bubbletea/issues/1440
 	// https://github.com/sst/opencode/issues/127
@@ -102,7 +104,7 @@ func (a appModel) Init() tea.Cmd {
 	return tea.Batch(cmds...)
 }
 
-func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	measure := util.Measure("app.Update")
 	defer measure("from", fmt.Sprintf("%T", msg))
 
@@ -499,6 +501,26 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		a.editor.SetExitKeyInDebounce(false)
 	case dialog.FindSelectedMsg:
 		return a.openFile(msg.FilePath)
+
+	// API
+	case api.Request:
+		slog.Info("api", "path", msg.Path)
+		var response any = true
+		switch msg.Path {
+		case "/tui/open-help":
+			helpDialog := dialog.NewHelpDialog(a.app)
+			a.modal = helpDialog
+		case "/tui/prompt":
+			var body struct {
+				Text  string          `json:"text"`
+				Parts []opencode.Part `json:"parts"`
+			}
+			json.Unmarshal((msg.Body), &body)
+			a.editor.SetValue(body.Text)
+		default:
+			break
+		}
+		cmds = append(cmds, api.Reply(context.Background(), a.app.Client, response))
 	}
 
 	s, cmd := a.status.Update(msg)
@@ -532,7 +554,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return a, tea.Batch(cmds...)
 }
 
-func (a appModel) View() string {
+func (a Model) View() string {
 	measure := util.Measure("app.View")
 	defer measure()
 	t := theme.CurrentTheme()
@@ -569,7 +591,7 @@ func (a appModel) View() string {
 	return mainLayout + "\n" + a.status.View()
 }
 
-func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
+func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
 	var cmd tea.Cmd
 	response, err := a.app.Client.File.Read(
 		context.Background(),
@@ -589,7 +611,7 @@ func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
 	return a, cmd
 }
 
-func (a appModel) home() string {
+func (a Model) home() string {
 	measure := util.Measure("home.View")
 	defer measure()
 	t := theme.CurrentTheme()
@@ -726,7 +748,7 @@ func (a appModel) home() string {
 	return mainLayout
 }
 
-func (a appModel) chat() string {
+func (a Model) chat() string {
 	measure := util.Measure("chat.View")
 	defer measure()
 	effectiveWidth := a.width - 4
@@ -774,7 +796,7 @@ func (a appModel) chat() string {
 	return mainLayout
 }
 
-func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
+func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
 	var cmd tea.Cmd
 	cmds := []tea.Cmd{
 		util.CmdHandler(commands.CommandExecutedMsg(command)),
@@ -1057,7 +1079,7 @@ func NewModel(app *app.App) tea.Model {
 		leaderBinding = &binding
 	}
 
-	model := &appModel{
+	model := &Model{
 		status:               status.NewStatusCmp(app),
 		app:                  app,
 		editor:               editor,

+ 4 - 4
packages/tui/sdk/.stats.yml

@@ -1,4 +1,4 @@
-configured_endpoints: 22
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-e7f4ac9b5afd5c6db4741a27b5445167808b0a3b7c36dfd525bfb3446a11a253.yml
-openapi_spec_hash: 3e7b367a173d6de7924f35a41ac6b5a5
-config_hash: 6d56a7ca0d6ed899ecdb5c053a8278ae
+configured_endpoints: 24
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d10809ab68e48a338167e5504d69db2a0a80739adf6ecd3f065644a4139bc374.yml
+openapi_spec_hash: 4875565ef8df3446dbab11f450e04c51
+config_hash: 0032a76356d31c6b4c218b39fff635bb

+ 18 - 1
packages/tui/sdk/api.md

@@ -19,7 +19,6 @@ Methods:
 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>
-- <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#LogLevel">LogLevel</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#Mode">Mode</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>
@@ -76,12 +75,23 @@ Methods:
 
 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#FilePartInputParam">FilePartInputParam</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#FilePartSourceUnionParam">FilePartSourceUnionParam</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#FilePartSourceTextParam">FilePartSourceTextParam</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#FileSourceParam">FileSourceParam</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#PartUnionParam">PartUnionParam</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#SnapshotPartParam">SnapshotPartParam</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#StepFinishPartParam">StepFinishPartParam</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#SymbolSourceParam">SymbolSourceParam</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#TextPartInputParam">TextPartInputParam</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#ToolPartParam">ToolPartParam</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#ToolStateCompletedParam">ToolStateCompletedParam</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#ToolStateErrorParam">ToolStateErrorParam</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#ToolStatePendingParam">ToolStatePendingParam</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#ToolStateRunningParam">ToolStateRunningParam</a>
 
 Response Types:
 
@@ -118,3 +128,10 @@ Methods:
 - <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>
+
+# Tui
+
+Methods:
+
+- <code title="post /tui/open-help">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.OpenHelp">OpenHelp</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>
+- <code title="post /tui/prompt">client.Tui.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#TuiService.Prompt">Prompt</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</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#TuiPromptParams">TuiPromptParams</a>) (<a href="https://pkg.go.dev/builtin#bool">bool</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>

+ 0 - 18
packages/tui/sdk/app.go

@@ -145,24 +145,6 @@ func (r appTimeJSON) RawJSON() string {
 	return r.raw
 }
 
-// Log level
-type LogLevel string
-
-const (
-	LogLevelDebug LogLevel = "DEBUG"
-	LogLevelInfo  LogLevel = "INFO"
-	LogLevelWarn  LogLevel = "WARN"
-	LogLevelError LogLevel = "ERROR"
-)
-
-func (r LogLevel) IsKnown() bool {
-	switch r {
-	case LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelError:
-		return true
-	}
-	return false
-}
-
 type Mode struct {
 	Name   string          `json:"name,required"`
 	Tools  map[string]bool `json:"tools,required"`

+ 2 - 0
packages/tui/sdk/client.go

@@ -22,6 +22,7 @@ type Client struct {
 	File    *FileService
 	Config  *ConfigService
 	Session *SessionService
+	Tui     *TuiService
 }
 
 // DefaultClientOptions read from the environment (OPENCODE_BASE_URL). This should
@@ -49,6 +50,7 @@ func NewClient(opts ...option.RequestOption) (r *Client) {
 	r.File = NewFileService(opts...)
 	r.Config = NewConfigService(opts...)
 	r.Session = NewSessionService(opts...)
+	r.Tui = NewTuiService(opts...)
 
 	return
 }

+ 0 - 3
packages/tui/sdk/config.go

@@ -57,8 +57,6 @@ type Config struct {
 	Keybinds KeybindsConfig `json:"keybinds"`
 	// @deprecated Always uses stretch layout.
 	Layout ConfigLayout `json:"layout"`
-	// Minimum log level to write to log files
-	LogLevel LogLevel `json:"log_level"`
 	// MCP (Model Context Protocol) server configurations
 	Mcp map[string]ConfigMcp `json:"mcp"`
 	// Modes configuration, see https://opencode.ai/docs/modes
@@ -90,7 +88,6 @@ type configJSON struct {
 	Instructions      apijson.Field
 	Keybinds          apijson.Field
 	Layout            apijson.Field
-	LogLevel          apijson.Field
 	Mcp               apijson.Field
 	Mode              apijson.Field
 	Model             apijson.Field

+ 253 - 0
packages/tui/sdk/session.go

@@ -483,6 +483,23 @@ func (r FilePartType) IsKnown() bool {
 	return false
 }
 
+type FilePartParam struct {
+	ID        param.Field[string]                   `json:"id,required"`
+	MessageID param.Field[string]                   `json:"messageID,required"`
+	Mime      param.Field[string]                   `json:"mime,required"`
+	SessionID param.Field[string]                   `json:"sessionID,required"`
+	Type      param.Field[FilePartType]             `json:"type,required"`
+	URL       param.Field[string]                   `json:"url,required"`
+	Filename  param.Field[string]                   `json:"filename"`
+	Source    param.Field[FilePartSourceUnionParam] `json:"source"`
+}
+
+func (r FilePartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r FilePartParam) implementsPartUnionParam() {}
+
 type FilePartInputParam struct {
 	Mime     param.Field[string]                   `json:"mime,required"`
 	Type     param.Field[FilePartInputType]        `json:"type,required"`
@@ -932,6 +949,38 @@ func (r PartType) IsKnown() bool {
 	return false
 }
 
+type PartParam struct {
+	ID        param.Field[string]                   `json:"id,required"`
+	MessageID param.Field[string]                   `json:"messageID,required"`
+	SessionID param.Field[string]                   `json:"sessionID,required"`
+	Type      param.Field[PartType]                 `json:"type,required"`
+	CallID    param.Field[string]                   `json:"callID"`
+	Cost      param.Field[float64]                  `json:"cost"`
+	Filename  param.Field[string]                   `json:"filename"`
+	Mime      param.Field[string]                   `json:"mime"`
+	Snapshot  param.Field[string]                   `json:"snapshot"`
+	Source    param.Field[FilePartSourceUnionParam] `json:"source"`
+	State     param.Field[interface{}]              `json:"state"`
+	Synthetic param.Field[bool]                     `json:"synthetic"`
+	Text      param.Field[string]                   `json:"text"`
+	Time      param.Field[interface{}]              `json:"time"`
+	Tokens    param.Field[interface{}]              `json:"tokens"`
+	Tool      param.Field[string]                   `json:"tool"`
+	URL       param.Field[string]                   `json:"url"`
+}
+
+func (r PartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r PartParam) implementsPartUnionParam() {}
+
+// Satisfied by [TextPartParam], [FilePartParam], [ToolPartParam],
+// [StepStartPartParam], [StepFinishPartParam], [SnapshotPartParam], [PartParam].
+type PartUnionParam interface {
+	implementsPartUnionParam()
+}
+
 type Session struct {
 	ID       string        `json:"id,required"`
 	Time     SessionTime   `json:"time,required"`
@@ -1074,6 +1123,20 @@ func (r SnapshotPartType) IsKnown() bool {
 	return false
 }
 
+type SnapshotPartParam struct {
+	ID        param.Field[string]           `json:"id,required"`
+	MessageID param.Field[string]           `json:"messageID,required"`
+	SessionID param.Field[string]           `json:"sessionID,required"`
+	Snapshot  param.Field[string]           `json:"snapshot,required"`
+	Type      param.Field[SnapshotPartType] `json:"type,required"`
+}
+
+func (r SnapshotPartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r SnapshotPartParam) implementsPartUnionParam() {}
+
 type StepFinishPart struct {
 	ID        string               `json:"id,required"`
 	Cost      float64              `json:"cost,required"`
@@ -1170,6 +1233,41 @@ func (r StepFinishPartType) IsKnown() bool {
 	return false
 }
 
+type StepFinishPartParam struct {
+	ID        param.Field[string]                    `json:"id,required"`
+	Cost      param.Field[float64]                   `json:"cost,required"`
+	MessageID param.Field[string]                    `json:"messageID,required"`
+	SessionID param.Field[string]                    `json:"sessionID,required"`
+	Tokens    param.Field[StepFinishPartTokensParam] `json:"tokens,required"`
+	Type      param.Field[StepFinishPartType]        `json:"type,required"`
+}
+
+func (r StepFinishPartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r StepFinishPartParam) implementsPartUnionParam() {}
+
+type StepFinishPartTokensParam struct {
+	Cache     param.Field[StepFinishPartTokensCacheParam] `json:"cache,required"`
+	Input     param.Field[float64]                        `json:"input,required"`
+	Output    param.Field[float64]                        `json:"output,required"`
+	Reasoning param.Field[float64]                        `json:"reasoning,required"`
+}
+
+func (r StepFinishPartTokensParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+type StepFinishPartTokensCacheParam struct {
+	Read  param.Field[float64] `json:"read,required"`
+	Write param.Field[float64] `json:"write,required"`
+}
+
+func (r StepFinishPartTokensCacheParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
 type StepStartPart struct {
 	ID        string            `json:"id,required"`
 	MessageID string            `json:"messageID,required"`
@@ -1212,6 +1310,19 @@ func (r StepStartPartType) IsKnown() bool {
 	return false
 }
 
+type StepStartPartParam struct {
+	ID        param.Field[string]            `json:"id,required"`
+	MessageID param.Field[string]            `json:"messageID,required"`
+	SessionID param.Field[string]            `json:"sessionID,required"`
+	Type      param.Field[StepStartPartType] `json:"type,required"`
+}
+
+func (r StepStartPartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r StepStartPartParam) implementsPartUnionParam() {}
+
 type SymbolSource struct {
 	Kind  int64              `json:"kind,required"`
 	Name  string             `json:"name,required"`
@@ -1439,6 +1550,31 @@ func (r textPartTimeJSON) RawJSON() string {
 	return r.raw
 }
 
+type TextPartParam struct {
+	ID        param.Field[string]            `json:"id,required"`
+	MessageID param.Field[string]            `json:"messageID,required"`
+	SessionID param.Field[string]            `json:"sessionID,required"`
+	Text      param.Field[string]            `json:"text,required"`
+	Type      param.Field[TextPartType]      `json:"type,required"`
+	Synthetic param.Field[bool]              `json:"synthetic"`
+	Time      param.Field[TextPartTimeParam] `json:"time"`
+}
+
+func (r TextPartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r TextPartParam) implementsPartUnionParam() {}
+
+type TextPartTimeParam struct {
+	Start param.Field[float64] `json:"start,required"`
+	End   param.Field[float64] `json:"end"`
+}
+
+func (r TextPartTimeParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
 type TextPartInputParam struct {
 	Text      param.Field[string]                 `json:"text,required"`
 	Type      param.Field[TextPartInputType]      `json:"type,required"`
@@ -1625,6 +1761,44 @@ func (r ToolPartType) IsKnown() bool {
 	return false
 }
 
+type ToolPartParam struct {
+	ID        param.Field[string]                  `json:"id,required"`
+	CallID    param.Field[string]                  `json:"callID,required"`
+	MessageID param.Field[string]                  `json:"messageID,required"`
+	SessionID param.Field[string]                  `json:"sessionID,required"`
+	State     param.Field[ToolPartStateUnionParam] `json:"state,required"`
+	Tool      param.Field[string]                  `json:"tool,required"`
+	Type      param.Field[ToolPartType]            `json:"type,required"`
+}
+
+func (r ToolPartParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r ToolPartParam) implementsPartUnionParam() {}
+
+type ToolPartStateParam struct {
+	Status   param.Field[ToolPartStateStatus] `json:"status,required"`
+	Error    param.Field[string]              `json:"error"`
+	Input    param.Field[interface{}]         `json:"input"`
+	Metadata param.Field[interface{}]         `json:"metadata"`
+	Output   param.Field[string]              `json:"output"`
+	Time     param.Field[interface{}]         `json:"time"`
+	Title    param.Field[string]              `json:"title"`
+}
+
+func (r ToolPartStateParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r ToolPartStateParam) implementsToolPartStateUnionParam() {}
+
+// Satisfied by [ToolStatePendingParam], [ToolStateRunningParam],
+// [ToolStateCompletedParam], [ToolStateErrorParam], [ToolPartStateParam].
+type ToolPartStateUnionParam interface {
+	implementsToolPartStateUnionParam()
+}
+
 type ToolStateCompleted struct {
 	Input    map[string]interface{}   `json:"input,required"`
 	Metadata map[string]interface{}   `json:"metadata,required"`
@@ -1695,6 +1869,30 @@ func (r toolStateCompletedTimeJSON) RawJSON() string {
 	return r.raw
 }
 
+type ToolStateCompletedParam struct {
+	Input    param.Field[map[string]interface{}]      `json:"input,required"`
+	Metadata param.Field[map[string]interface{}]      `json:"metadata,required"`
+	Output   param.Field[string]                      `json:"output,required"`
+	Status   param.Field[ToolStateCompletedStatus]    `json:"status,required"`
+	Time     param.Field[ToolStateCompletedTimeParam] `json:"time,required"`
+	Title    param.Field[string]                      `json:"title,required"`
+}
+
+func (r ToolStateCompletedParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r ToolStateCompletedParam) implementsToolPartStateUnionParam() {}
+
+type ToolStateCompletedTimeParam struct {
+	End   param.Field[float64] `json:"end,required"`
+	Start param.Field[float64] `json:"start,required"`
+}
+
+func (r ToolStateCompletedTimeParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
 type ToolStateError struct {
 	Error  string                 `json:"error,required"`
 	Input  map[string]interface{} `json:"input,required"`
@@ -1760,6 +1958,28 @@ func (r toolStateErrorTimeJSON) RawJSON() string {
 	return r.raw
 }
 
+type ToolStateErrorParam struct {
+	Error  param.Field[string]                  `json:"error,required"`
+	Input  param.Field[map[string]interface{}]  `json:"input,required"`
+	Status param.Field[ToolStateErrorStatus]    `json:"status,required"`
+	Time   param.Field[ToolStateErrorTimeParam] `json:"time,required"`
+}
+
+func (r ToolStateErrorParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r ToolStateErrorParam) implementsToolPartStateUnionParam() {}
+
+type ToolStateErrorTimeParam struct {
+	End   param.Field[float64] `json:"end,required"`
+	Start param.Field[float64] `json:"start,required"`
+}
+
+func (r ToolStateErrorTimeParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
 type ToolStatePending struct {
 	Status ToolStatePendingStatus `json:"status,required"`
 	JSON   toolStatePendingJSON   `json:"-"`
@@ -1797,6 +2017,16 @@ func (r ToolStatePendingStatus) IsKnown() bool {
 	return false
 }
 
+type ToolStatePendingParam struct {
+	Status param.Field[ToolStatePendingStatus] `json:"status,required"`
+}
+
+func (r ToolStatePendingParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r ToolStatePendingParam) implementsToolPartStateUnionParam() {}
+
 type ToolStateRunning struct {
 	Status   ToolStateRunningStatus `json:"status,required"`
 	Time     ToolStateRunningTime   `json:"time,required"`
@@ -1863,6 +2093,28 @@ func (r toolStateRunningTimeJSON) RawJSON() string {
 	return r.raw
 }
 
+type ToolStateRunningParam struct {
+	Status   param.Field[ToolStateRunningStatus]    `json:"status,required"`
+	Time     param.Field[ToolStateRunningTimeParam] `json:"time,required"`
+	Input    param.Field[interface{}]               `json:"input"`
+	Metadata param.Field[map[string]interface{}]    `json:"metadata"`
+	Title    param.Field[string]                    `json:"title"`
+}
+
+func (r ToolStateRunningParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
+func (r ToolStateRunningParam) implementsToolPartStateUnionParam() {}
+
+type ToolStateRunningTimeParam struct {
+	Start param.Field[float64] `json:"start,required"`
+}
+
+func (r ToolStateRunningTimeParam) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}
+
 type UserMessage struct {
 	ID        string          `json:"id,required"`
 	Role      UserMessageRole `json:"role,required"`
@@ -1954,6 +2206,7 @@ type SessionChatParams struct {
 	ProviderID param.Field[string]                       `json:"providerID,required"`
 	MessageID  param.Field[string]                       `json:"messageID"`
 	Mode       param.Field[string]                       `json:"mode"`
+	Tools      param.Field[map[string]bool]              `json:"tools"`
 }
 
 func (r SessionChatParams) MarshalJSON() (data []byte, err error) {

+ 3 - 0
packages/tui/sdk/session_test.go

@@ -131,6 +131,9 @@ func TestSessionChatWithOptionalParams(t *testing.T) {
 			ProviderID: opencode.F("providerID"),
 			MessageID:  opencode.F("msg"),
 			Mode:       opencode.F("mode"),
+			Tools: opencode.F(map[string]bool{
+				"foo": true,
+			}),
 		},
 	)
 	if err != nil {

+ 57 - 0
packages/tui/sdk/tui.go

@@ -0,0 +1,57 @@
+// 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/param"
+	"github.com/sst/opencode-sdk-go/internal/requestconfig"
+	"github.com/sst/opencode-sdk-go/option"
+)
+
+// TuiService 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 [NewTuiService] method instead.
+type TuiService struct {
+	Options []option.RequestOption
+}
+
+// NewTuiService 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 NewTuiService(opts ...option.RequestOption) (r *TuiService) {
+	r = &TuiService{}
+	r.Options = opts
+	return
+}
+
+// Open the help dialog
+func (r *TuiService) OpenHelp(ctx context.Context, opts ...option.RequestOption) (res *bool, err error) {
+	opts = append(r.Options[:], opts...)
+	path := "tui/open-help"
+	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...)
+	return
+}
+
+// Send a prompt to the TUI
+func (r *TuiService) Prompt(ctx context.Context, body TuiPromptParams, opts ...option.RequestOption) (res *bool, err error) {
+	opts = append(r.Options[:], opts...)
+	path := "tui/prompt"
+	err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...)
+	return
+}
+
+type TuiPromptParams struct {
+	Parts param.Field[[]PartUnionParam] `json:"parts,required"`
+	Text  param.Field[string]           `json:"text,required"`
+}
+
+func (r TuiPromptParams) MarshalJSON() (data []byte, err error) {
+	return apijson.MarshalRoot(r)
+}

+ 72 - 0
packages/tui/sdk/tui_test.go

@@ -0,0 +1,72 @@
+// 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 TestTuiOpenHelp(t *testing.T) {
+	t.Skip("skipped: tests are disabled for the time being")
+	baseURL := "http://localhost:4010"
+	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+		baseURL = envURL
+	}
+	if !testutil.CheckTestServer(t, baseURL) {
+		return
+	}
+	client := opencode.NewClient(
+		option.WithBaseURL(baseURL),
+	)
+	_, err := client.Tui.OpenHelp(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 TestTuiPrompt(t *testing.T) {
+	t.Skip("skipped: tests are disabled for the time being")
+	baseURL := "http://localhost:4010"
+	if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
+		baseURL = envURL
+	}
+	if !testutil.CheckTestServer(t, baseURL) {
+		return
+	}
+	client := opencode.NewClient(
+		option.WithBaseURL(baseURL),
+	)
+	_, err := client.Tui.Prompt(context.TODO(), opencode.TuiPromptParams{
+		Parts: opencode.F([]opencode.PartUnionParam{opencode.TextPartParam{
+			ID:        opencode.F("id"),
+			MessageID: opencode.F("messageID"),
+			SessionID: opencode.F("sessionID"),
+			Text:      opencode.F("text"),
+			Type:      opencode.F(opencode.TextPartTypeText),
+			Synthetic: opencode.F(true),
+			Time: opencode.F(opencode.TextPartTimeParam{
+				Start: opencode.F(0.000000),
+				End:   opencode.F(0.000000),
+			}),
+		}}),
+		Text: opencode.F("text"),
+	})
+	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())
+	}
+}

+ 5 - 0
stainless.yml

@@ -121,6 +121,11 @@ resources:
       messages: get /session/{id}/message
       chat: post /session/{id}/message
 
+  tui:
+    methods:
+      prompt: post /tui/prompt
+      openHelp: post /tui/open-help
+
 settings:
   disable_mock_tests: true
   license: Apache-2.0