summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-07-21 19:53:22 -0400
committerDax Raad <[email protected]>2025-07-21 19:53:58 -0400
commitf20ef61bc72ad830549c8a885f063b47e4a75557 (patch)
tree3232940032f5c8e2a25b036c0290b22dc407cb63
parent5611ef8b28216aa9dd2ceb6ed17d5779a29154f6 (diff)
downloadopencode-f20ef61bc72ad830549c8a885f063b47e4a75557.tar.gz
opencode-f20ef61bc72ad830549c8a885f063b47e4a75557.zip
wip: api for tui
-rw-r--r--packages/opencode/src/cli/cmd/serve.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui.ts15
-rw-r--r--packages/opencode/src/server/server.ts43
-rw-r--r--packages/opencode/src/server/tui.ts30
-rw-r--r--packages/opencode/src/util/queue.ts19
-rw-r--r--packages/tui/cmd/opencode/main.go3
-rw-r--r--packages/tui/internal/api/api.go41
-rw-r--r--packages/tui/internal/tui/tui.go40
-rw-r--r--packages/tui/sdk/.stats.yml8
-rw-r--r--packages/tui/sdk/api.md19
-rw-r--r--packages/tui/sdk/app.go18
-rw-r--r--packages/tui/sdk/client.go2
-rw-r--r--packages/tui/sdk/config.go3
-rw-r--r--packages/tui/sdk/session.go253
-rw-r--r--packages/tui/sdk/session_test.go3
-rw-r--r--packages/tui/sdk/tui.go57
-rw-r--r--packages/tui/sdk/tui_test.go72
-rw-r--r--stainless.yml5
18 files changed, 594 insertions, 39 deletions
diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts
index 6dab38f18..0e13ddbd3 100644
--- a/packages/opencode/src/cli/cmd/serve.ts
+++ b/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,
diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts
index 388aa743c..791faadd0 100644
--- a/packages/opencode/src/cli/cmd/tui.ts
+++ b/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"]
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 4e6ebfbb3..a3b34f41f 100644
--- a/packages/opencode/src/server/server.ts
+++ b/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
}
diff --git a/packages/opencode/src/server/tui.ts b/packages/opencode/src/server/tui.ts
new file mode 100644
index 000000000..60ac5eef5
--- /dev/null
+++ b/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)
+ })
diff --git a/packages/opencode/src/util/queue.ts b/packages/opencode/src/util/queue.ts
new file mode 100644
index 000000000..259d785ce
--- /dev/null
+++ b/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()
+ }
+}
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index 8e387d21b..a882d9daf 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/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
diff --git a/packages/tui/internal/api/api.go b/packages/tui/internal/api/api.go
new file mode 100644
index 000000000..b4d3adee2
--- /dev/null
+++ b/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
+ }
+}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index bd0eaeada..4e654c0c8 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/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,
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index 3aa07cb68..56337c0cc 100644
--- a/packages/tui/sdk/.stats.yml
+++ b/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
diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md
index 242fadc99..983e13499 100644
--- a/packages/tui/sdk/api.md
+++ b/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>
diff --git a/packages/tui/sdk/app.go b/packages/tui/sdk/app.go
index aa47e83b2..407de0617 100644
--- a/packages/tui/sdk/app.go
+++ b/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"`
diff --git a/packages/tui/sdk/client.go b/packages/tui/sdk/client.go
index 955eb7d6b..6baf21a8f 100644
--- a/packages/tui/sdk/client.go
+++ b/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
}
diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go
index b824e5788..0461cba87 100644
--- a/packages/tui/sdk/config.go
+++ b/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
diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go
index bdee22aea..3ab2343eb 100644
--- a/packages/tui/sdk/session.go
+++ b/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) {
diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go
index b96a98b9d..5d7c55cad 100644
--- a/packages/tui/sdk/session_test.go
+++ b/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 {
diff --git a/packages/tui/sdk/tui.go b/packages/tui/sdk/tui.go
new file mode 100644
index 000000000..c1396d233
--- /dev/null
+++ b/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)
+}
diff --git a/packages/tui/sdk/tui_test.go b/packages/tui/sdk/tui_test.go
new file mode 100644
index 000000000..620f2121c
--- /dev/null
+++ b/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())
+ }
+}
diff --git a/stainless.yml b/stainless.yml
index 48941320f..66a2fea1a 100644
--- a/stainless.yml
+++ b/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