summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-07-10 09:53:18 -0500
committeradamdottv <[email protected]>2025-07-10 10:06:51 -0500
commitce4cb820f72591d58ea78d1c0d955a7ca50a0217 (patch)
treeeaf4d26f01c7bd6f4c737cabfd6484ab5abd3401
parentba5be6b6257ea06302db70e3f706e0e29359a77d (diff)
downloadopencode-ce4cb820f72591d58ea78d1c0d955a7ca50a0217.tar.gz
opencode-ce4cb820f72591d58ea78d1c0d955a7ca50a0217.zip
feat(tui): modes
-rw-r--r--packages/opencode/src/cli/cmd/tui.ts2
-rw-r--r--packages/opencode/src/config/config.ts1
-rw-r--r--packages/opencode/src/server/server.ts1
-rw-r--r--packages/opencode/src/session/index.ts2
-rw-r--r--packages/opencode/src/session/mode.ts1
-rw-r--r--packages/tui/cmd/opencode/main.go12
-rw-r--r--packages/tui/internal/app/app.go95
-rw-r--r--packages/tui/internal/commands/command.go7
-rw-r--r--packages/tui/internal/components/status/status.go135
-rw-r--r--packages/tui/internal/config/config.go21
-rw-r--r--packages/tui/internal/tui/tui.go11
-rw-r--r--packages/tui/sdk/.stats.yml8
-rw-r--r--packages/tui/sdk/api.md2
-rw-r--r--packages/tui/sdk/app.go56
-rw-r--r--packages/tui/sdk/app_test.go22
-rw-r--r--packages/tui/sdk/config.go78
-rw-r--r--packages/tui/sdk/session.go53
-rw-r--r--packages/tui/sdk/session_test.go6
-rwxr-xr-xscripts/stainless4
-rw-r--r--stainless.yml2
20 files changed, 431 insertions, 88 deletions
diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts
index 1e8d2bbcf..d18824f78 100644
--- a/packages/opencode/src/cli/cmd/tui.ts
+++ b/packages/opencode/src/cli/cmd/tui.ts
@@ -11,6 +11,7 @@ import { Config } from "../../config/config"
import { Bus } from "../../bus"
import { Log } from "../../util/log"
import { FileWatcher } from "../../file/watch"
+import { Mode } from "../../session/mode"
export const TuiCommand = cmd({
command: "$0 [project]",
@@ -87,6 +88,7 @@ export const TuiCommand = cmd({
CGO_ENABLED: "0",
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
+ OPENCODE_MODES: JSON.stringify(await Mode.list()),
},
onExit: () => {
server.stop()
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 9d6ca2ca4..b9614a3f5 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -70,6 +70,7 @@ export namespace Config {
.object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
app_help: z.string().optional().default("<leader>h").describe("Show help dialog"),
+ switch_mode: z.string().optional().default("tab").describe("Switch mode"),
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
session_new: z.string().optional().default("<leader>n").describe("Create a new session"),
session_list: z.string().optional().default("<leader>l").describe("List all sessions"),
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index db6a8fdf6..a958f48b6 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -448,6 +448,7 @@ export namespace Server {
z.object({
providerID: z.string(),
modelID: z.string(),
+ mode: z.string(),
parts: MessageV2.UserPart.array(),
}),
),
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index a43cedd2c..ea0d65911 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -284,7 +284,7 @@ export namespace Session {
sessionID: string
providerID: string
modelID: string
- mode?: string
+ mode: string
parts: MessageV2.UserPart[]
}) {
const l = log.clone().tag("session", input.sessionID)
diff --git a/packages/opencode/src/session/mode.ts b/packages/opencode/src/session/mode.ts
index e7134f45a..c7c0304ca 100644
--- a/packages/opencode/src/session/mode.ts
+++ b/packages/opencode/src/session/mode.ts
@@ -30,6 +30,7 @@ export namespace Mode {
write: false,
edit: false,
patch: false,
+ bash: false,
},
},
},
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index 31ef03e5a..cd827a952 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/packages/tui/cmd/opencode/main.go
@@ -39,6 +39,14 @@ func main() {
os.Exit(1)
}
+ modesStr := os.Getenv("OPENCODE_MODES")
+ var modes []opencode.Mode
+ err = json.Unmarshal([]byte(modesStr), &modes)
+ if err != nil {
+ slog.Error("Failed to unmarshal modes", "error", err)
+ os.Exit(1)
+ }
+
httpClient := opencode.NewClient(
option.WithBaseURL(url),
)
@@ -47,7 +55,7 @@ func main() {
logger := slog.New(apiHandler)
slog.SetDefault(logger)
- slog.Debug("TUI launched", "app", appInfo)
+ slog.Debug("TUI launched", "app", appInfoStr, "modes", modesStr)
go func() {
err = clipboard.Init()
@@ -60,7 +68,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- app_, err := app.New(ctx, version, appInfo, httpClient, model, prompt)
+ app_, err := app.New(ctx, version, appInfo, modes, httpClient, model, prompt)
if err != nil {
panic(err)
}
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index bd16036af..b41bbaeaf 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -23,11 +23,15 @@ import (
type App struct {
Info opencode.App
+ Modes []opencode.Mode
+ Providers []opencode.Provider
Version string
StatePath string
Config *opencode.Config
Client *opencode.Client
State *config.State
+ ModeIndex int
+ Mode *opencode.Mode
Provider *opencode.Provider
Model *opencode.Model
Session *opencode.Session
@@ -64,6 +68,7 @@ func New(
ctx context.Context,
version string,
appInfo opencode.App,
+ modes []opencode.Mode,
httpClient *opencode.Client,
model *string,
prompt *string,
@@ -87,14 +92,33 @@ func New(
config.SaveState(appStatePath, appState)
}
+ if appState.ModeModel == nil {
+ appState.ModeModel = make(map[string]config.ModeModel)
+ }
+
if configInfo.Theme != "" {
appState.Theme = configInfo.Theme
}
- if configInfo.Model != "" {
- splits := strings.Split(configInfo.Model, "/")
- appState.Provider = splits[0]
- appState.Model = strings.Join(splits[1:], "/")
+ var modeIndex int
+ var mode *opencode.Mode
+ modeName := "build"
+ if appState.Mode != "" {
+ modeName = appState.Mode
+ }
+ for i, m := range modes {
+ if m.Name == modeName {
+ modeIndex = i
+ break
+ }
+ }
+ mode = &modes[modeIndex]
+
+ if mode.Model.ModelID != "" {
+ appState.ModeModel[mode.Name] = config.ModeModel{
+ ProviderID: mode.Model.ProviderID,
+ ModelID: mode.Model.ModelID,
+ }
}
if err := theme.LoadThemesFromDirectories(
@@ -119,11 +143,14 @@ func New(
app := &App{
Info: appInfo,
+ Modes: modes,
Version: version,
StatePath: appStatePath,
Config: configInfo,
State: appState,
Client: httpClient,
+ ModeIndex: modeIndex,
+ Mode: mode,
Session: &opencode.Session{},
Messages: []opencode.MessageUnion{},
Commands: commands.LoadFromConfig(configInfo),
@@ -162,6 +189,45 @@ func (a *App) SetClipboard(text string) tea.Cmd {
return tea.Sequence(cmds...)
}
+func (a *App) SwitchMode() (*App, tea.Cmd) {
+ a.ModeIndex++
+ if a.ModeIndex >= len(a.Modes) {
+ a.ModeIndex = 0
+ }
+ a.Mode = &a.Modes[a.ModeIndex]
+
+ modelID := a.Mode.Model.ModelID
+ providerID := a.Mode.Model.ProviderID
+ if modelID == "" {
+ if model, ok := a.State.ModeModel[a.Mode.Name]; ok {
+ modelID = model.ModelID
+ providerID = model.ProviderID
+ }
+ }
+
+ if modelID != "" {
+ for _, provider := range a.Providers {
+ if provider.ID == providerID {
+ a.Provider = &provider
+ for _, model := range provider.Models {
+ if model.ID == modelID {
+ a.Model = &model
+ break
+ }
+ }
+ break
+ }
+ }
+ }
+
+ a.State.Mode = a.Mode.Name
+
+ return a, func() tea.Msg {
+ a.SaveState()
+ return nil
+ }
+}
+
func (a *App) InitializeProvider() tea.Cmd {
providersResponse, err := a.Client.Config.Providers(context.Background())
if err != nil {
@@ -198,6 +264,14 @@ func (a *App) InitializeProvider() tea.Cmd {
return nil
}
+ a.Providers = providers
+
+ // retains backwards compatibility with old state format
+ if model, ok := a.State.ModeModel[a.State.Mode]; ok {
+ a.State.Provider = model.ProviderID
+ a.State.Model = model.ModelID
+ }
+
var currentProvider *opencode.Provider
var currentModel *opencode.Model
for _, provider := range providers {
@@ -322,10 +396,14 @@ func (a *App) CompactSession(ctx context.Context) tea.Cmd {
a.compactCancel = nil
}()
- _, err := a.Client.Session.Summarize(compactCtx, a.Session.ID, opencode.SessionSummarizeParams{
- ProviderID: opencode.F(a.Provider.ID),
- ModelID: opencode.F(a.Model.ID),
- })
+ _, err := a.Client.Session.Summarize(
+ compactCtx,
+ a.Session.ID,
+ opencode.SessionSummarizeParams{
+ ProviderID: opencode.F(a.Provider.ID),
+ ModelID: opencode.F(a.Model.ID),
+ },
+ )
if err != nil {
if compactCtx.Err() != context.Canceled {
slog.Error("Failed to compact session", "error", err)
@@ -417,6 +495,7 @@ func (a *App) SendChatMessage(
Parts: opencode.F(parts),
ProviderID: opencode.F(a.Provider.ID),
ModelID: opencode.F(a.Model.ID),
+ Mode: opencode.F(a.Mode.Name),
})
if err != nil {
errormsg := fmt.Sprintf("failed to send message: %v", err)
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index 791f74759..1659adb85 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -86,6 +86,7 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command {
const (
AppHelpCommand CommandName = "app_help"
+ SwitchModeCommand CommandName = "switch_mode"
EditorOpenCommand CommandName = "editor_open"
SessionNewCommand CommandName = "session_new"
SessionListCommand CommandName = "session_list"
@@ -153,6 +154,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Trigger: []string{"help"},
},
{
+ Name: SwitchModeCommand,
+ Description: "switch mode",
+ Keybindings: parseBindings("tab"),
+ Trigger: []string{"mode"},
+ },
+ {
Name: EditorOpenCommand,
Description: "open editor",
Keybindings: parseBindings("<leader>e"),
diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go
index d0d61b173..0809114d0 100644
--- a/packages/tui/internal/components/status/status.go
+++ b/packages/tui/internal/components/status/status.go
@@ -7,8 +7,9 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
- "github.com/sst/opencode-sdk-go"
+ "github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/app"
+ "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
@@ -55,7 +56,12 @@ func (m statusComponent) logo() string {
Render(open + code + version)
}
-func formatTokensAndCost(tokens float64, contextWindow float64, cost float64, isSubscriptionModel bool) string {
+func formatTokensAndCost(
+ tokens float64,
+ contextWindow float64,
+ cost float64,
+ isSubscriptionModel bool,
+) string {
// Format tokens in human-readable format (e.g., 110K, 1.2M)
var formattedTokens string
switch {
@@ -104,50 +110,103 @@ func (m statusComponent) View() string {
Padding(0, 1).
Render(m.cwd)
- sessionInfo := ""
- if m.app.Session.ID != "" {
- tokens := float64(0)
- cost := float64(0)
- contextWindow := m.app.Model.Limit.Context
-
- for _, message := range m.app.Messages {
- if assistant, ok := message.(opencode.AssistantMessage); ok {
- cost += assistant.Cost
- usage := assistant.Tokens
- if usage.Output > 0 {
- if assistant.Summary {
- tokens = usage.Output
- continue
- }
- tokens = (usage.Input +
- usage.Cache.Write +
- usage.Cache.Read +
- usage.Output +
- usage.Reasoning)
- }
- }
- }
-
- // Check if current model is a subscription model (cost is 0 for both input and output)
- isSubscriptionModel := m.app.Model != nil &&
- m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0
-
- sessionInfo = styles.NewStyle().
- Foreground(t.TextMuted()).
- Background(t.BackgroundElement()).
- Padding(0, 1).
- Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel))
+ // sessionInfo := ""
+ // if m.app.Session.ID != "" {
+ // tokens := float64(0)
+ // cost := float64(0)
+ // contextWindow := m.app.Model.Limit.Context
+ //
+ // for _, message := range m.app.Messages {
+ // if assistant, ok := message.(opencode.AssistantMessage); ok {
+ // cost += assistant.Cost
+ // usage := assistant.Tokens
+ // if usage.Output > 0 {
+ // if assistant.Summary {
+ // tokens = usage.Output
+ // continue
+ // }
+ // tokens = (usage.Input +
+ // usage.Cache.Write +
+ // usage.Cache.Read +
+ // usage.Output +
+ // usage.Reasoning)
+ // }
+ // }
+ // }
+ //
+ // // Check if current model is a subscription model (cost is 0 for both input and output)
+ // isSubscriptionModel := m.app.Model != nil &&
+ // m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0
+ //
+ // sessionInfo = styles.NewStyle().
+ // Foreground(t.TextMuted()).
+ // Background(t.BackgroundElement()).
+ // Padding(0, 1).
+ // Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel))
+ // }
+
+ var modeBackground compat.AdaptiveColor
+ var modeForeground compat.AdaptiveColor
+ switch m.app.ModeIndex {
+ case 0:
+ modeBackground = t.BackgroundElement()
+ modeForeground = t.TextMuted()
+ case 1:
+ modeBackground = t.Secondary()
+ modeForeground = t.BackgroundPanel()
+ case 2:
+ modeBackground = t.Accent()
+ modeForeground = t.BackgroundPanel()
+ case 3:
+ modeBackground = t.Success()
+ modeForeground = t.BackgroundPanel()
+ case 4:
+ modeBackground = t.Warning()
+ modeForeground = t.BackgroundPanel()
+ case 5:
+ modeBackground = t.Primary()
+ modeForeground = t.BackgroundPanel()
+ case 6:
+ modeBackground = t.Error()
+ modeForeground = t.BackgroundPanel()
+ default:
+ modeBackground = t.Secondary()
+ modeForeground = t.BackgroundPanel()
+ }
+
+ command := m.app.Commands[commands.SwitchModeCommand]
+ kb := command.Keybindings[0]
+ key := kb.Key
+ if kb.RequiresLeader {
+ key = m.app.Config.Keybinds.Leader + " " + kb.Key
}
- // diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics())
+ modeStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground)
+ modeNameStyle := modeStyle.Bold(true).Render
+ modeDescStyle := modeStyle.Render
+ mode := modeNameStyle(strings.ToUpper(m.app.Mode.Name)) + modeDescStyle(" MODE")
+ mode = modeStyle.
+ Padding(0, 1).
+ BorderLeft(true).
+ BorderStyle(lipgloss.ThickBorder()).
+ BorderForeground(modeBackground).
+ BorderBackground(t.BackgroundPanel()).
+ Render(mode)
+
+ mode = styles.NewStyle().
+ Faint(true).
+ Background(t.BackgroundPanel()).
+ Foreground(t.TextMuted()).
+ Render(key+" ") +
+ mode
space := max(
0,
- m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
+ m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(mode),
)
spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
- status := logo + cwd + spacer + sessionInfo
+ status := logo + cwd + spacer + mode
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
return blank + "\n" + status
diff --git a/packages/tui/internal/config/config.go b/packages/tui/internal/config/config.go
index 3dd6fcf59..7004b85b1 100644
--- a/packages/tui/internal/config/config.go
+++ b/packages/tui/internal/config/config.go
@@ -16,18 +16,27 @@ type ModelUsage struct {
LastUsed time.Time `toml:"last_used"`
}
+type ModeModel struct {
+ ProviderID string `toml:"provider_id"`
+ ModelID string `toml:"model_id"`
+}
+
type State struct {
- Theme string `toml:"theme"`
- Provider string `toml:"provider"`
- Model string `toml:"model"`
- RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
- MessagesRight bool `toml:"messages_right"`
- SplitDiff bool `toml:"split_diff"`
+ Theme string `toml:"theme"`
+ ModeModel map[string]ModeModel `toml:"mode_model"`
+ Provider string `toml:"provider"`
+ Model string `toml:"model"`
+ Mode string `toml:"mode"`
+ RecentlyUsedModels []ModelUsage `toml:"recently_used_models"`
+ MessagesRight bool `toml:"messages_right"`
+ SplitDiff bool `toml:"split_diff"`
}
func NewState() *State {
return &State{
Theme: "opencode",
+ Mode: "build",
+ ModeModel: make(map[string]ModeModel),
RecentlyUsedModels: make([]ModelUsage, 0),
}
}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 770e8ac01..0a075a146 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -23,6 +23,7 @@ import (
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/components/status"
"github.com/sst/opencode/internal/components/toast"
+ "github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -524,8 +525,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider
a.app.Model = &msg.Model
- a.app.State.Provider = msg.Provider.ID
- a.app.State.Model = msg.Model.ID
+ a.app.State.ModeModel[a.app.Mode.Name] = config.ModeModel{
+ ProviderID: msg.Provider.ID,
+ ModelID: msg.Model.ID,
+ }
a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID)
a.app.SaveState()
case dialog.ThemeSelectedMsg:
@@ -823,6 +826,10 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
case commands.AppHelpCommand:
helpDialog := dialog.NewHelpDialog(a.app)
a.modal = helpDialog
+ case commands.SwitchModeCommand:
+ updated, cmd := a.app.SwitchMode()
+ a.app = updated
+ cmds = append(cmds, cmd)
case commands.EditorOpenCommand:
if a.app.IsBusy() {
// status.Warn("Agent is working, please wait...")
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index e3d985793..225a0e490 100644
--- a/packages/tui/sdk/.stats.yml
+++ b/packages/tui/sdk/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 21
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-879570c29c56e0a73a0624a84662b7f7c319a3c790c78ec6ac4cf62a7b1a5bd0.yml
-openapi_spec_hash: 2432e2dfed22193a0c6b3dfe0f82ec7d
-config_hash: 53e3aeb355f3b2e0d10985d6d7635a7e
+configured_endpoints: 22
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-37247433660e125f9a79ff173d8007f13192d28a40f4e599e002446a6ed0c128.yml
+openapi_spec_hash: 8095ebe2d88259381a58e7b0c87244c4
+config_hash: 589ec6a935a43a3c49a325ece86cbda2
diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md
index c0304614a..15bdcfa93 100644
--- a/packages/tui/sdk/api.md
+++ b/packages/tui/sdk/api.md
@@ -20,12 +20,14 @@ 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>
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>
- <code title="post /log">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Log">Log</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#AppLogParams">AppLogParams</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 /mode">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Modes">Modes</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#Mode">Mode</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Find
diff --git a/packages/tui/sdk/app.go b/packages/tui/sdk/app.go
index ba9303c80..09d8cbf39 100644
--- a/packages/tui/sdk/app.go
+++ b/packages/tui/sdk/app.go
@@ -55,6 +55,14 @@ func (r *AppService) Log(ctx context.Context, body AppLogParams, opts ...option.
return
}
+// List all modes
+func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (res *[]Mode, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "mode"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
type App struct {
Git bool `json:"git,required"`
Hostname string `json:"hostname,required"`
@@ -149,6 +157,54 @@ func (r LogLevel) IsKnown() bool {
return false
}
+type Mode struct {
+ Name string `json:"name,required"`
+ Tools map[string]bool `json:"tools,required"`
+ Model ModeModel `json:"model"`
+ Prompt string `json:"prompt"`
+ JSON modeJSON `json:"-"`
+}
+
+// modeJSON contains the JSON metadata for the struct [Mode]
+type modeJSON struct {
+ Name apijson.Field
+ Tools apijson.Field
+ Model apijson.Field
+ Prompt apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Mode) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modeJSON) RawJSON() string {
+ return r.raw
+}
+
+type ModeModel struct {
+ ModelID string `json:"modelID,required"`
+ ProviderID string `json:"providerID,required"`
+ JSON modeModelJSON `json:"-"`
+}
+
+// modeModelJSON contains the JSON metadata for the struct [ModeModel]
+type modeModelJSON struct {
+ ModelID apijson.Field
+ ProviderID apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ModeModel) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modeModelJSON) RawJSON() string {
+ return r.raw
+}
+
type AppLogParams struct {
// Log level
Level param.Field[AppLogParamsLevel] `json:"level,required"`
diff --git a/packages/tui/sdk/app_test.go b/packages/tui/sdk/app_test.go
index 15f531ad4..8028f2c53 100644
--- a/packages/tui/sdk/app_test.go
+++ b/packages/tui/sdk/app_test.go
@@ -85,3 +85,25 @@ func TestAppLogWithOptionalParams(t *testing.T) {
t.Fatalf("err should be nil: %s", err.Error())
}
}
+
+func TestAppModes(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.Modes(context.TODO())
+ if err != nil {
+ var apierr *opencode.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go
index d6dc65f25..59c4e62cf 100644
--- a/packages/tui/sdk/config.go
+++ b/packages/tui/sdk/config.go
@@ -65,7 +65,8 @@ type Config struct {
// 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"`
+ Mcp map[string]ConfigMcp `json:"mcp"`
+ Mode ConfigMode `json:"mode"`
// Model to use in the format of provider/model, eg anthropic/claude-2
Model string `json:"model"`
// Custom provider configurations and model overrides
@@ -86,6 +87,7 @@ type configJSON struct {
Keybinds apijson.Field
LogLevel apijson.Field
Mcp apijson.Field
+ Mode apijson.Field
Model apijson.Field
Provider apijson.Field
Theme apijson.Field
@@ -276,6 +278,77 @@ func (r ConfigMcpType) IsKnown() bool {
return false
}
+type ConfigMode struct {
+ Build ConfigModeBuild `json:"build"`
+ Plan ConfigModePlan `json:"plan"`
+ ExtraFields map[string]ConfigMode `json:"-,extras"`
+ JSON configModeJSON `json:"-"`
+}
+
+// configModeJSON contains the JSON metadata for the struct [ConfigMode]
+type configModeJSON struct {
+ Build apijson.Field
+ Plan apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigMode) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configModeJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigModeBuild struct {
+ Model string `json:"model"`
+ Prompt string `json:"prompt"`
+ Tools map[string]bool `json:"tools"`
+ JSON configModeBuildJSON `json:"-"`
+}
+
+// configModeBuildJSON contains the JSON metadata for the struct [ConfigModeBuild]
+type configModeBuildJSON struct {
+ Model apijson.Field
+ Prompt apijson.Field
+ Tools apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigModeBuild) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configModeBuildJSON) RawJSON() string {
+ return r.raw
+}
+
+type ConfigModePlan struct {
+ Model string `json:"model"`
+ Prompt string `json:"prompt"`
+ Tools map[string]bool `json:"tools"`
+ JSON configModePlanJSON `json:"-"`
+}
+
+// configModePlanJSON contains the JSON metadata for the struct [ConfigModePlan]
+type configModePlanJSON struct {
+ Model apijson.Field
+ Prompt apijson.Field
+ Tools apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ConfigModePlan) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r configModePlanJSON) RawJSON() string {
+ return r.raw
+}
+
type ConfigProvider struct {
Models map[string]ConfigProviderModel `json:"models,required"`
ID string `json:"id"`
@@ -460,6 +533,8 @@ type Keybinds struct {
SessionShare string `json:"session_share,required"`
// Unshare current session
SessionUnshare string `json:"session_unshare,required"`
+ // Switch mode
+ SwitchMode string `json:"switch_mode,required"`
// List available themes
ThemeList string `json:"theme_list,required"`
// Toggle tool details
@@ -500,6 +575,7 @@ type keybindsJSON struct {
SessionNew apijson.Field
SessionShare apijson.Field
SessionUnshare apijson.Field
+ SwitchMode apijson.Field
ThemeList apijson.Field
ToolDetails apijson.Field
raw string
diff --git a/packages/tui/sdk/session.go b/packages/tui/sdk/session.go
index fc55c6912..1f19b97f5 100644
--- a/packages/tui/sdk/session.go
+++ b/packages/tui/sdk/session.go
@@ -439,11 +439,12 @@ type AssistantMessagePart struct {
Type AssistantMessagePartType `json:"type,required"`
ID string `json:"id"`
// This field can have the runtime type of [ToolPartState].
- State interface{} `json:"state"`
- Text string `json:"text"`
- Tool string `json:"tool"`
- JSON assistantMessagePartJSON `json:"-"`
- union AssistantMessagePartUnion
+ State interface{} `json:"state"`
+ Synthetic bool `json:"synthetic"`
+ Text string `json:"text"`
+ Tool string `json:"tool"`
+ JSON assistantMessagePartJSON `json:"-"`
+ union AssistantMessagePartUnion
}
// assistantMessagePartJSON contains the JSON metadata for the struct
@@ -452,6 +453,7 @@ type assistantMessagePartJSON struct {
Type apijson.Field
ID apijson.Field
State apijson.Field
+ Synthetic apijson.Field
Text apijson.Field
Tool apijson.Field
raw string
@@ -815,15 +817,17 @@ func (r StepStartPartType) IsKnown() bool {
}
type TextPart struct {
- Text string `json:"text,required"`
- Type TextPartType `json:"type,required"`
- JSON textPartJSON `json:"-"`
+ Text string `json:"text,required"`
+ Type TextPartType `json:"type,required"`
+ Synthetic bool `json:"synthetic"`
+ JSON textPartJSON `json:"-"`
}
// textPartJSON contains the JSON metadata for the struct [TextPart]
type textPartJSON struct {
Text apijson.Field
Type apijson.Field
+ Synthetic apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
@@ -855,8 +859,9 @@ func (r TextPartType) IsKnown() bool {
}
type TextPartParam struct {
- Text param.Field[string] `json:"text,required"`
- Type param.Field[TextPartType] `json:"type,required"`
+ Text param.Field[string] `json:"text,required"`
+ Type param.Field[TextPartType] `json:"type,required"`
+ Synthetic param.Field[bool] `json:"synthetic"`
}
func (r TextPartParam) MarshalJSON() (data []byte, err error) {
@@ -1311,13 +1316,14 @@ func (r userMessageTimeJSON) RawJSON() string {
}
type UserMessagePart struct {
- Type UserMessagePartType `json:"type,required"`
- Filename string `json:"filename"`
- Mime string `json:"mime"`
- Text string `json:"text"`
- URL string `json:"url"`
- JSON userMessagePartJSON `json:"-"`
- union UserMessagePartUnion
+ Type UserMessagePartType `json:"type,required"`
+ Filename string `json:"filename"`
+ Mime string `json:"mime"`
+ Synthetic bool `json:"synthetic"`
+ Text string `json:"text"`
+ URL string `json:"url"`
+ JSON userMessagePartJSON `json:"-"`
+ union UserMessagePartUnion
}
// userMessagePartJSON contains the JSON metadata for the struct [UserMessagePart]
@@ -1325,6 +1331,7 @@ type userMessagePartJSON struct {
Type apijson.Field
Filename apijson.Field
Mime apijson.Field
+ Synthetic apijson.Field
Text apijson.Field
URL apijson.Field
raw string
@@ -1390,11 +1397,12 @@ func (r UserMessagePartType) IsKnown() bool {
}
type UserMessagePartParam struct {
- Type param.Field[UserMessagePartType] `json:"type,required"`
- Filename param.Field[string] `json:"filename"`
- Mime param.Field[string] `json:"mime"`
- Text param.Field[string] `json:"text"`
- URL param.Field[string] `json:"url"`
+ Type param.Field[UserMessagePartType] `json:"type,required"`
+ Filename param.Field[string] `json:"filename"`
+ Mime param.Field[string] `json:"mime"`
+ Synthetic param.Field[bool] `json:"synthetic"`
+ Text param.Field[string] `json:"text"`
+ URL param.Field[string] `json:"url"`
}
func (r UserMessagePartParam) MarshalJSON() (data []byte, err error) {
@@ -1409,6 +1417,7 @@ type UserMessagePartUnionParam interface {
}
type SessionChatParams struct {
+ Mode param.Field[string] `json:"mode,required"`
ModelID param.Field[string] `json:"modelID,required"`
Parts param.Field[[]UserMessagePartUnionParam] `json:"parts,required"`
ProviderID param.Field[string] `json:"providerID,required"`
diff --git a/packages/tui/sdk/session_test.go b/packages/tui/sdk/session_test.go
index 15137e62c..4ff2818c5 100644
--- a/packages/tui/sdk/session_test.go
+++ b/packages/tui/sdk/session_test.go
@@ -117,10 +117,12 @@ func TestSessionChat(t *testing.T) {
context.TODO(),
"id",
opencode.SessionChatParams{
+ Mode: opencode.F("mode"),
ModelID: opencode.F("modelID"),
Parts: opencode.F([]opencode.UserMessagePartUnionParam{opencode.TextPartParam{
- Text: opencode.F("text"),
- Type: opencode.F(opencode.TextPartTypeText),
+ Text: opencode.F("text"),
+ Type: opencode.F(opencode.TextPartTypeText),
+ Synthetic: opencode.F(true),
}}),
ProviderID: opencode.F("providerID"),
},
diff --git a/scripts/stainless b/scripts/stainless
index dae9016f3..6554268fe 100755
--- a/scripts/stainless
+++ b/scripts/stainless
@@ -9,8 +9,8 @@ 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 "Fetching OpenAPI spec from http://127.0.0.1:4096/doc..."
+curl -s http://127.0.0.1:4096/doc > openapi.json
echo "Stopping server..."
kill $SERVER_PID
diff --git a/stainless.yml b/stainless.yml
index 9cc29c066..aec6817f5 100644
--- a/stainless.yml
+++ b/stainless.yml
@@ -49,10 +49,12 @@ resources:
models:
app: App
logLevel: LogLevel
+ mode: Mode
methods:
get: get /app
init: post /app/init
log: post /log
+ modes: get /mode
find:
models: