summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-07-16 12:42:52 -0500
committeradamdotdevin <[email protected]>2025-07-16 12:43:02 -0500
commitcdc1d8a94d12a31d5d162cb37744305738177ac2 (patch)
treea9a9494ddc5b4a1a30c28e9ef19c99541292e630 /packages
parentfdd6d6600f090ad4f15b2bbfc0bb75c36a722f4e (diff)
downloadopencode-cdc1d8a94d12a31d5d162cb37744305738177ac2.tar.gz
opencode-cdc1d8a94d12a31d5d162cb37744305738177ac2.zip
feat(tui): layout config to render full width
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/config/config.ts9
-rw-r--r--packages/tui/internal/app/app.go6
-rw-r--r--packages/tui/internal/app/constants.go4
-rw-r--r--packages/tui/internal/commands/command.go12
-rw-r--r--packages/tui/internal/components/chat/editor.go25
-rw-r--r--packages/tui/internal/components/chat/message.go21
-rw-r--r--packages/tui/internal/components/chat/messages.go122
-rw-r--r--packages/tui/internal/tui/tui.go149
-rw-r--r--packages/tui/sdk/.stats.yml6
-rw-r--r--packages/tui/sdk/api.md16
-rw-r--r--packages/tui/sdk/app.go147
-rw-r--r--packages/tui/sdk/app_test.go22
-rw-r--r--packages/tui/sdk/config.go304
-rw-r--r--packages/tui/sdk/config_test.go22
-rw-r--r--packages/web/src/content/docs/docs/troubleshooting.mdx17
15 files changed, 456 insertions, 426 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index c22bd7ca4..87f325f17 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -125,6 +125,11 @@ export namespace Config {
ref: "KeybindsConfig",
})
+ export const Layout = z.enum(["auto", "stretch"]).openapi({
+ ref: "LayoutConfig",
+ })
+ export type Layout = z.infer<typeof Layout>
+
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -151,7 +156,8 @@ export namespace Config {
plan: Mode.optional(),
})
.catchall(Mode)
- .optional(),
+ .optional()
+ .describe("Modes configuration, see https://opencode.ai/docs/modes"),
log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
provider: z
.record(
@@ -164,6 +170,7 @@ export namespace Config {
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
+ layout: Layout.optional().describe("Layout to use for the TUI"),
experimental: z
.object({
hook: z
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 818203b98..011d5a89a 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -260,7 +260,7 @@ func (a *App) SwitchModeReverse() (*App, tea.Cmd) {
}
func (a *App) InitializeProvider() tea.Cmd {
- providersResponse, err := a.Client.Config.Providers(context.Background())
+ providersResponse, err := a.Client.App.Providers(context.Background())
if err != nil {
slog.Error("Failed to list providers", "error", err)
// TODO: notify user
@@ -355,7 +355,7 @@ func (a *App) InitializeProvider() tea.Cmd {
}
func getDefaultModel(
- response *opencode.ConfigProvidersResponse,
+ response *opencode.AppProvidersResponse,
provider opencode.Provider,
) *opencode.Model {
if match, ok := response.Default[provider.ID]; ok {
@@ -618,7 +618,7 @@ func (a *App) ListMessages(ctx context.Context, sessionId string) ([]Message, er
}
func (a *App) ListProviders(ctx context.Context) ([]opencode.Provider, error) {
- response, err := a.Client.Config.Providers(ctx)
+ response, err := a.Client.App.Providers(ctx)
if err != nil {
return nil, err
}
diff --git a/packages/tui/internal/app/constants.go b/packages/tui/internal/app/constants.go
new file mode 100644
index 000000000..0d37833f5
--- /dev/null
+++ b/packages/tui/internal/app/constants.go
@@ -0,0 +1,4 @@
+package app
+
+const MAX_CONTAINER_WIDTH = 86
+const EDIT_DIFF_MAX_WIDTH = 180
diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go
index 6ceec5fa1..6af7a4568 100644
--- a/packages/tui/internal/commands/command.go
+++ b/packages/tui/internal/commands/command.go
@@ -230,12 +230,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
Keybindings: parseBindings("<leader>t"),
Trigger: []string{"themes"},
},
- {
- Name: FileListCommand,
- Description: "list files",
- Keybindings: parseBindings("<leader>f"),
- Trigger: []string{"files"},
- },
+ // {
+ // Name: FileListCommand,
+ // Description: "list files",
+ // Keybindings: parseBindings("<leader>f"),
+ // Trigger: []string{"files"},
+ // },
{
Name: FileCloseCommand,
Description: "close file",
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 67f1f75ea..de74b4895 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -27,8 +27,8 @@ import (
type EditorComponent interface {
tea.Model
- View(width int) string
- Content(width int) string
+ tea.ViewModel
+ Content() string
Lines() int
Value() string
Length() int
@@ -46,6 +46,7 @@ type EditorComponent interface {
type editorComponent struct {
app *app.App
+ width int
textarea textarea.Model
spinner spinner.Model
interruptKeyInDebounce bool
@@ -61,6 +62,12 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = min(msg.Width-4, app.MAX_CONTAINER_WIDTH)
+ if m.app.Config.Layout == opencode.LayoutConfigStretch {
+ m.width = msg.Width - 4
+ }
+ return m, nil
case spinner.TickMsg:
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
@@ -227,7 +234,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m *editorComponent) Content(width int) string {
+func (m *editorComponent) Content() string {
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
@@ -236,7 +243,7 @@ func (m *editorComponent) Content(width int) string {
Bold(true)
prompt := promptStyle.Render(">")
- m.textarea.SetWidth(width - 6)
+ m.textarea.SetWidth(m.width - 6)
textarea := lipgloss.JoinHorizontal(
lipgloss.Top,
prompt,
@@ -248,7 +255,7 @@ func (m *editorComponent) Content(width int) string {
}
textarea = styles.NewStyle().
Background(t.BackgroundElement()).
- Width(width).
+ Width(m.width).
PaddingTop(1).
PaddingBottom(1).
BorderStyle(lipgloss.ThickBorder()).
@@ -284,7 +291,7 @@ func (m *editorComponent) Content(width int) string {
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
}
- space := width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
+ space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
info := hint + spacer + model
@@ -294,10 +301,10 @@ func (m *editorComponent) Content(width int) string {
return content
}
-func (m *editorComponent) View(width int) string {
+func (m *editorComponent) View() string {
if m.Lines() > 1 {
return lipgloss.Place(
- width,
+ m.width,
5,
lipgloss.Center,
lipgloss.Center,
@@ -305,7 +312,7 @@ func (m *editorComponent) View(width int) string {
styles.WhitespaceStyle(theme.CurrentTheme().Background()),
)
}
- return m.Content(width)
+ return m.Content()
}
func (m *editorComponent) Focused() bool {
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 35ff675bd..d3263b053 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -10,6 +10,7 @@ import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
+ "github.com/muesli/reflow/truncate"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/diff"
@@ -319,11 +320,19 @@ func renderToolDetails(
if diffField != nil {
patch := diffField.(string)
var formattedDiff string
- formattedDiff, _ = diff.FormatUnifiedDiff(
- filename,
- patch,
- diff.WithWidth(width-2),
- )
+ if width < 120 {
+ formattedDiff, _ = diff.FormatUnifiedDiff(
+ filename,
+ patch,
+ diff.WithWidth(width-2),
+ )
+ } else {
+ formattedDiff, _ = diff.FormatDiff(
+ filename,
+ patch,
+ diff.WithWidth(width-2),
+ )
+ }
body = strings.TrimSpace(formattedDiff)
style := styles.NewStyle().
Background(backgroundColor).
@@ -551,6 +560,8 @@ func renderToolTitle(
toolName := renderToolName(toolCall.Tool)
title = fmt.Sprintf("%s %s", toolName, toolArgs)
}
+
+ title = truncate.StringWithTail(title, uint(width-6), "...")
return title
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 739f69109..e17847645 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -19,8 +19,7 @@ import (
type MessagesComponent interface {
tea.Model
- View(width, height int) string
- SetWidth(width int) tea.Cmd
+ tea.ViewModel
PageUp() (tea.Model, tea.Cmd)
PageDown() (tea.Model, tea.Cmd)
HalfPageUp() (tea.Model, tea.Cmd)
@@ -32,8 +31,9 @@ type MessagesComponent interface {
}
type messagesComponent struct {
- width int
+ width, height int
app *app.App
+ header string
viewport viewport.Model
cache *PartCache
rendering bool
@@ -53,6 +53,17 @@ func (m *messagesComponent) Init() tea.Cmd {
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ effectiveWidth := msg.Width - 4
+ // Clear cache on resize since width affects rendering
+ if m.width != effectiveWidth {
+ m.cache.Clear()
+ }
+ m.width = effectiveWidth
+ m.height = msg.Height - 7
+ m.viewport.SetWidth(m.width)
+ m.header = m.renderHeader()
+ return m, m.Reload()
case app.SendMsg:
m.viewport.GotoBottom()
m.tail = true
@@ -82,21 +93,18 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionUpdated:
if msg.Properties.Info.ID == m.app.Session.ID {
- m.renderView(m.width)
- if m.tail {
- m.viewport.GotoBottom()
- }
+ m.header = m.renderHeader()
}
case opencode.EventListResponseEventMessageUpdated:
if msg.Properties.Info.SessionID == m.app.Session.ID {
- m.renderView(m.width)
+ m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
}
case opencode.EventListResponseEventMessagePartUpdated:
if msg.Properties.Part.SessionID == m.app.Session.ID {
- m.renderView(m.width)
+ m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
@@ -111,10 +119,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m *messagesComponent) renderView(width int) {
+func (m *messagesComponent) renderView() {
measure := util.Measure("messages.renderView")
defer measure("messageCount", len(m.app.Messages))
+ m.header = m.renderHeader()
+
t := theme.CurrentTheme()
blocks := make([]string, 0)
m.partCount = 0
@@ -122,6 +132,11 @@ func (m *messagesComponent) renderView(width int) {
orphanedToolCalls := make([]opencode.ToolPart, 0)
+ width := min(m.width, app.MAX_CONTAINER_WIDTH)
+ if m.app.Config.Layout == opencode.LayoutConfigStretch {
+ width = m.width
+ }
+
for _, message := range m.app.Messages {
var content string
var cached bool
@@ -185,6 +200,12 @@ func (m *messagesComponent) renderView(width int) {
width,
files,
)
+ content = lipgloss.PlaceHorizontal(
+ m.width,
+ lipgloss.Center,
+ content,
+ styles.WhitespaceStyle(t.Background()),
+ )
m.cache.Set(key, content)
}
if content != "" {
@@ -246,6 +267,12 @@ func (m *messagesComponent) renderView(width int) {
"",
toolCallParts...,
)
+ content = lipgloss.PlaceHorizontal(
+ m.width,
+ lipgloss.Center,
+ content,
+ styles.WhitespaceStyle(t.Background()),
+ )
m.cache.Set(key, content)
}
} else {
@@ -259,6 +286,12 @@ func (m *messagesComponent) renderView(width int) {
"",
toolCallParts...,
)
+ content = lipgloss.PlaceHorizontal(
+ m.width,
+ lipgloss.Center,
+ content,
+ styles.WhitespaceStyle(t.Background()),
+ )
}
if content != "" {
m.partCount++
@@ -273,6 +306,13 @@ func (m *messagesComponent) renderView(width int) {
continue
}
+ width := width
+ if m.app.Config.Layout == opencode.LayoutConfigAuto &&
+ part.Tool == "edit" &&
+ part.State.Error == "" {
+ width = min(m.width, app.EDIT_DIFF_MAX_WIDTH)
+ }
+
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
key := m.cache.GenerateKey(casted.ID,
part.ID,
@@ -286,6 +326,12 @@ func (m *messagesComponent) renderView(width int) {
part,
width,
)
+ content = lipgloss.PlaceHorizontal(
+ m.width,
+ lipgloss.Center,
+ content,
+ styles.WhitespaceStyle(t.Background()),
+ )
m.cache.Set(key, content)
}
} else {
@@ -295,6 +341,12 @@ func (m *messagesComponent) renderView(width int) {
part,
width,
)
+ content = lipgloss.PlaceHorizontal(
+ m.width,
+ lipgloss.Center,
+ content,
+ styles.WhitespaceStyle(t.Background()),
+ )
}
if content != "" {
m.partCount++
@@ -333,22 +385,27 @@ func (m *messagesComponent) renderView(width int) {
}
}
+ m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
-
}
-func (m *messagesComponent) header(width int) string {
+func (m *messagesComponent) renderHeader() string {
if m.app.Session.ID == "" {
return ""
}
+ headerWidth := min(m.width, app.MAX_CONTAINER_WIDTH)
+ if m.app.Config.Layout == opencode.LayoutConfigStretch {
+ headerWidth = m.width
+ }
+
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
headerLines := []string{}
headerLines = append(
headerLines,
- util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
+ util.ToMarkdown("# "+m.app.Session.Title, headerWidth-6, t.Background()),
)
share := ""
@@ -397,7 +454,7 @@ func (m *messagesComponent) header(width int) string {
Direction: layout.Row,
Justify: layout.JustifySpaceBetween,
Align: layout.AlignStretch,
- Width: width - 6,
+ Width: headerWidth - 6,
},
layout.FlexItem{
View: share,
@@ -408,12 +465,10 @@ func (m *messagesComponent) header(width int) string {
)
headerLines = append(headerLines, share)
-
header := strings.Join(headerLines, "\n")
-
header = styles.NewStyle().
Background(t.Background()).
- Width(width).
+ Width(headerWidth).
PaddingLeft(2).
PaddingRight(2).
BorderLeft(true).
@@ -422,6 +477,12 @@ func (m *messagesComponent) header(width int) string {
BorderForeground(t.BackgroundElement()).
BorderStyle(lipgloss.ThickBorder()).
Render(header)
+ header = lipgloss.PlaceHorizontal(
+ m.width,
+ lipgloss.Center,
+ header,
+ styles.WhitespaceStyle(t.Background()),
+ )
return "\n" + header + "\n"
}
@@ -473,44 +534,27 @@ func formatTokensAndCost(
)
}
-func (m *messagesComponent) View(width, height int) string {
+func (m *messagesComponent) View() string {
t := theme.CurrentTheme()
if m.rendering {
return lipgloss.Place(
- width,
- height,
+ m.width,
+ m.height,
lipgloss.Center,
lipgloss.Center,
styles.NewStyle().Background(t.Background()).Render(""),
styles.WhitespaceStyle(t.Background()),
)
}
- header := m.header(width)
- m.viewport.SetWidth(width)
- m.viewport.SetHeight(height - lipgloss.Height(header))
return styles.NewStyle().
Background(t.Background()).
- Render(header + "\n" + m.viewport.View())
-}
-
-func (m *messagesComponent) SetWidth(width int) tea.Cmd {
- if m.width == width {
- return nil
- }
- // Clear cache on resize since width affects rendering
- if m.width != width {
- m.cache.Clear()
- }
- m.width = width
- m.viewport.SetWidth(width)
- m.renderView(width)
- return nil
+ Render(m.header + "\n" + m.viewport.View())
}
func (m *messagesComponent) Reload() tea.Cmd {
return func() tea.Msg {
- m.renderView(m.width)
+ m.renderView()
return renderFinishedMsg{}
}
}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index eb7d1d3c5..6210ab7b1 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -56,7 +56,6 @@ const (
const interruptDebounceTimeout = 1 * time.Second
const exitDebounceTimeout = 1 * time.Second
-const fileViewerFullWidthCutoff = 160
type appModel struct {
width, height int
@@ -77,10 +76,6 @@ type appModel struct {
exitKeyState ExitKeyState
messagesRight bool
fileViewer fileviewer.Model
- lastMouse tea.Mouse
- fileViewerStart int
- fileViewerEnd int
- fileViewerHit bool
}
func (a appModel) Init() tea.Cmd {
@@ -288,30 +283,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, cmd
case tea.MouseWheelMsg:
if a.modal != nil {
- return a, nil
- }
-
- var cmd tea.Cmd
- if a.fileViewerHit {
- a.fileViewer, cmd = a.fileViewer.Update(msg)
- cmds = append(cmds, cmd)
- } else {
- updated, cmd := a.messages.Update(msg)
- a.messages = updated.(chat.MessagesComponent)
+ u, cmd := a.modal.Update(msg)
+ a.modal = u.(layout.Modal)
cmds = append(cmds, cmd)
+ return a, tea.Batch(cmds...)
}
+ updated, cmd := a.messages.Update(msg)
+ a.messages = updated.(chat.MessagesComponent)
+ cmds = append(cmds, cmd)
return a, tea.Batch(cmds...)
- case tea.MouseMotionMsg:
- a.lastMouse = msg.Mouse()
- a.fileViewerHit = a.fileViewer.HasFile() &&
- a.lastMouse.X > a.fileViewerStart &&
- a.lastMouse.X < a.fileViewerEnd
- case tea.MouseClickMsg:
- a.lastMouse = msg.Mouse()
- a.fileViewerHit = a.fileViewer.HasFile() &&
- a.lastMouse.X > a.fileViewerStart &&
- a.lastMouse.X < a.fileViewerEnd
case tea.BackgroundColorMsg:
styles.Terminal = &styles.TerminalInfo{
Background: msg.Color,
@@ -458,14 +439,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height
- container := min(a.width, 104)
- if a.fileViewer.HasFile() {
- if a.width < fileViewerFullWidthCutoff {
- container = a.width
- } else {
- container = min(min(a.width, max(a.width/2, 50)), 104)
- }
- }
+ container := min(a.width, app.MAX_CONTAINER_WIDTH)
layout.Current = &layout.LayoutInfo{
Viewport: layout.Dimensions{
Width: a.width,
@@ -475,21 +449,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Width: container,
},
}
- mainWidth := layout.Current.Container.Width
- a.messages.SetWidth(mainWidth - 4)
-
- sideWidth := a.width - mainWidth
- if a.width < fileViewerFullWidthCutoff {
- sideWidth = a.width
- }
- a.fileViewerStart = mainWidth
- a.fileViewerEnd = a.fileViewerStart + sideWidth
- if a.messagesRight {
- a.fileViewerStart = 0
- a.fileViewerEnd = sideWidth
- }
- a.fileViewer, cmd = a.fileViewer.SetSize(sideWidth, layout.Current.Viewport.Height)
- cmds = append(cmds, cmd)
case app.SessionSelectedMsg:
messages, err := a.app.ListMessages(context.Background(), msg.ID)
if err != nil {
@@ -569,48 +528,22 @@ func (a appModel) View() string {
t := theme.CurrentTheme()
var mainLayout string
- mainWidth := layout.Current.Container.Width - 4
+
if a.app.Session.ID == "" {
- mainLayout = a.home(mainWidth)
+ mainLayout = a.home()
} else {
- mainLayout = a.chat(mainWidth)
+ mainLayout = a.chat()
}
mainLayout = styles.NewStyle().
Background(t.Background()).
Padding(0, 2).
Render(mainLayout)
-
- mainHeight := lipgloss.Height(mainLayout)
-
- if a.fileViewer.HasFile() {
- file := a.fileViewer.View()
- baseStyle := styles.NewStyle().Background(t.BackgroundPanel())
- sidePanel := baseStyle.Height(mainHeight).Render(file)
- if a.width >= fileViewerFullWidthCutoff {
- if a.messagesRight {
- mainLayout = lipgloss.JoinHorizontal(
- lipgloss.Top,
- sidePanel,
- mainLayout,
- )
- } else {
- mainLayout = lipgloss.JoinHorizontal(
- lipgloss.Top,
- mainLayout,
- sidePanel,
- )
- }
- } else {
- mainLayout = sidePanel
- }
- } else {
- mainLayout = lipgloss.PlaceHorizontal(
- a.width,
- lipgloss.Center,
- mainLayout,
- styles.WhitespaceStyle(t.Background()),
- )
- }
+ mainLayout = lipgloss.PlaceHorizontal(
+ a.width,
+ lipgloss.Center,
+ mainLayout,
+ styles.WhitespaceStyle(t.Background()),
+ )
mainStyle := styles.NewStyle().Background(t.Background())
mainLayout = mainStyle.Render(mainLayout)
@@ -646,8 +579,9 @@ func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
return a, cmd
}
-func (a appModel) home(width int) string {
+func (a appModel) home() string {
t := theme.CurrentTheme()
+ effectiveWidth := a.width - 4
baseStyle := styles.NewStyle().Background(t.Background())
base := baseStyle.Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
@@ -678,7 +612,7 @@ func (a appModel) home(width int) string {
logoAndVersion := strings.Join([]string{logo, version}, "\n")
logoAndVersion = lipgloss.PlaceHorizontal(
- width,
+ effectiveWidth,
lipgloss.Center,
logoAndVersion,
styles.WhitespaceStyle(t.Background()),
@@ -689,7 +623,7 @@ func (a appModel) home(width int) string {
cmdcomp.WithLimit(6),
)
cmds := lipgloss.PlaceHorizontal(
- width,
+ effectiveWidth,
lipgloss.Center,
commandsView.View(),
styles.WhitespaceStyle(t.Background()),
@@ -701,19 +635,16 @@ func (a appModel) home(width int) string {
lines = append(lines, logoAndVersion)
lines = append(lines, "")
lines = append(lines, "")
- // lines = append(lines, base("cwd ")+muted(cwd))
- // lines = append(lines, base("config ")+muted(config))
- // lines = append(lines, "")
lines = append(lines, cmds)
lines = append(lines, "")
lines = append(lines, "")
mainHeight := lipgloss.Height(strings.Join(lines, "\n"))
- editorWidth := min(width, 80)
- editorView := a.editor.View(editorWidth)
+ editorView := a.editor.View()
+ editorWidth := lipgloss.Width(editorView)
editorView = lipgloss.PlaceHorizontal(
- width,
+ effectiveWidth,
lipgloss.Center,
editorView,
styles.WhitespaceStyle(t.Background()),
@@ -723,7 +654,7 @@ func (a appModel) home(width int) string {
editorLines := a.editor.Lines()
mainLayout := lipgloss.Place(
- width,
+ effectiveWidth,
a.height,
lipgloss.Center,
lipgloss.Center,
@@ -731,14 +662,14 @@ func (a appModel) home(width int) string {
styles.WhitespaceStyle(t.Background()),
)
- editorX := (width - editorWidth) / 2
+ editorX := (effectiveWidth - editorWidth) / 2
editorY := (a.height / 2) + (mainHeight / 2) - 2
if editorLines > 1 {
mainLayout = layout.PlaceOverlay(
editorX,
editorY,
- a.editor.Content(editorWidth),
+ a.editor.Content(),
mainLayout,
)
}
@@ -759,23 +690,31 @@ func (a appModel) home(width int) string {
return mainLayout
}
-func (a appModel) chat(width int) string {
- editorView := a.editor.View(width)
+func (a appModel) chat() string {
+ effectiveWidth := a.width - 4
+ t := theme.CurrentTheme()
+ editorView := a.editor.View()
lines := a.editor.Lines()
- messagesView := a.messages.View(width, a.height-5)
+ messagesView := a.messages.View()
editorWidth := lipgloss.Width(editorView)
editorHeight := max(lines, 5)
+ editorView = lipgloss.PlaceHorizontal(
+ effectiveWidth,
+ lipgloss.Center,
+ editorView,
+ styles.WhitespaceStyle(t.Background()),
+ )
mainLayout := messagesView + "\n" + editorView
- editorX := (a.width - editorWidth) / 2
+ editorX := (effectiveWidth - editorWidth) / 2
if lines > 1 {
editorY := a.height - editorHeight
mainLayout = layout.PlaceOverlay(
editorX,
editorY,
- a.editor.Content(width),
+ a.editor.Content(),
mainLayout,
)
}
@@ -968,11 +907,11 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
case commands.ThemeListCommand:
themeDialog := dialog.NewThemeDialog()
a.modal = themeDialog
- case commands.FileListCommand:
- a.editor.Blur()
- findDialog := dialog.NewFindDialog(a.fileProvider)
- cmds = append(cmds, findDialog.Init())
- a.modal = findDialog
+ // case commands.FileListCommand:
+ // a.editor.Blur()
+ // findDialog := dialog.NewFindDialog(a.fileProvider)
+ // cmds = append(cmds, findDialog.Init())
+ // a.modal = findDialog
case commands.FileCloseCommand:
a.fileViewer, cmd = a.fileViewer.Clear()
cmds = append(cmds, cmd)
diff --git a/packages/tui/sdk/.stats.yml b/packages/tui/sdk/.stats.yml
index 805d02803..22b27ac6d 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-7270b9e4859010d6680bcc92afcd6f7c679d80a2645f65d7097d19ce2e8cdc5a.yml
-openapi_spec_hash: 5fcbfaedebfea62c17c74437a9728b04
-config_hash: 38041c37df28a1c4383718e6d148dd0a
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-8792f91dd070f7b4ee671fc86e8a03976dc7fb6ee49f8c99ad989e1597003774.yml
+openapi_spec_hash: fe9dc3a074be560de0b97df9b5af2c1b
+config_hash: b7f3d9742335715c458494988498b183
diff --git a/packages/tui/sdk/api.md b/packages/tui/sdk/api.md
index 9d41e7522..1c3f5a5ee 100644
--- a/packages/tui/sdk/api.md
+++ b/packages/tui/sdk/api.md
@@ -21,6 +21,9 @@ 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>
+- <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#AppProvidersResponse">AppProvidersResponse</a>
Methods:
@@ -28,6 +31,7 @@ Methods:
- <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>
+- <code title="get /config/providers">client.App.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#AppProvidersResponse">AppProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Find
@@ -59,17 +63,15 @@ 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#Config">Config</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Keybinds">Keybinds</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpLocal">McpLocal</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#McpRemote">McpRemote</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Model">Model</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Provider">Provider</a>
-- <a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigProvidersResponse">ConfigProvidersResponse</a>
+- <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#KeybindsConfig">KeybindsConfig</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#LayoutConfig">LayoutConfig</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#McpLocalConfig">McpLocalConfig</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#McpRemoteConfig">McpRemoteConfig</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#ModeConfig">ModeConfig</a>
Methods:
- <code title="get /config">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Get">Get</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#Config">Config</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
-- <code title="get /config/providers">client.Config.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigService.Providers">Providers</a>(ctx <a href="https://pkg.go.dev/context">context</a>.<a href="https://pkg.go.dev/context#Context">Context</a>) (<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go">opencode</a>.<a href="https://pkg.go.dev/github.com/sst/opencode-sdk-go#ConfigProvidersResponse">ConfigProvidersResponse</a>, <a href="https://pkg.go.dev/builtin#error">error</a>)</code>
# Session
diff --git a/packages/tui/sdk/app.go b/packages/tui/sdk/app.go
index 69414f20b..aa47e83b2 100644
--- a/packages/tui/sdk/app.go
+++ b/packages/tui/sdk/app.go
@@ -63,6 +63,14 @@ func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (r
return
}
+// List all providers
+func (r *AppService) Providers(ctx context.Context, opts ...option.RequestOption) (res *AppProvidersResponse, err error) {
+ opts = append(r.Options[:], opts...)
+ path := "config/providers"
+ err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
+ return
+}
+
type App struct {
Git bool `json:"git,required"`
Hostname string `json:"hostname,required"`
@@ -203,6 +211,145 @@ func (r modeModelJSON) RawJSON() string {
return r.raw
}
+type Model struct {
+ ID string `json:"id,required"`
+ Attachment bool `json:"attachment,required"`
+ Cost ModelCost `json:"cost,required"`
+ Limit ModelLimit `json:"limit,required"`
+ Name string `json:"name,required"`
+ Options map[string]interface{} `json:"options,required"`
+ Reasoning bool `json:"reasoning,required"`
+ ReleaseDate string `json:"release_date,required"`
+ Temperature bool `json:"temperature,required"`
+ ToolCall bool `json:"tool_call,required"`
+ JSON modelJSON `json:"-"`
+}
+
+// modelJSON contains the JSON metadata for the struct [Model]
+type modelJSON struct {
+ ID apijson.Field
+ Attachment apijson.Field
+ Cost apijson.Field
+ Limit apijson.Field
+ Name apijson.Field
+ Options apijson.Field
+ Reasoning apijson.Field
+ ReleaseDate apijson.Field
+ Temperature apijson.Field
+ ToolCall apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Model) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelJSON) RawJSON() string {
+ return r.raw
+}
+
+type ModelCost struct {
+ Input float64 `json:"input,required"`
+ Output float64 `json:"output,required"`
+ CacheRead float64 `json:"cache_read"`
+ CacheWrite float64 `json:"cache_write"`
+ JSON modelCostJSON `json:"-"`
+}
+
+// modelCostJSON contains the JSON metadata for the struct [ModelCost]
+type modelCostJSON struct {
+ Input apijson.Field
+ Output apijson.Field
+ CacheRead apijson.Field
+ CacheWrite apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ModelCost) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelCostJSON) RawJSON() string {
+ return r.raw
+}
+
+type ModelLimit struct {
+ Context float64 `json:"context,required"`
+ Output float64 `json:"output,required"`
+ JSON modelLimitJSON `json:"-"`
+}
+
+// modelLimitJSON contains the JSON metadata for the struct [ModelLimit]
+type modelLimitJSON struct {
+ Context apijson.Field
+ Output apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *ModelLimit) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r modelLimitJSON) RawJSON() string {
+ return r.raw
+}
+
+type Provider struct {
+ ID string `json:"id,required"`
+ Env []string `json:"env,required"`
+ Models map[string]Model `json:"models,required"`
+ Name string `json:"name,required"`
+ API string `json:"api"`
+ Npm string `json:"npm"`
+ JSON providerJSON `json:"-"`
+}
+
+// providerJSON contains the JSON metadata for the struct [Provider]
+type providerJSON struct {
+ ID apijson.Field
+ Env apijson.Field
+ Models apijson.Field
+ Name apijson.Field
+ API apijson.Field
+ Npm apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *Provider) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r providerJSON) RawJSON() string {
+ return r.raw
+}
+
+type AppProvidersResponse struct {
+ Default map[string]string `json:"default,required"`
+ Providers []Provider `json:"providers,required"`
+ JSON appProvidersResponseJSON `json:"-"`
+}
+
+// appProvidersResponseJSON contains the JSON metadata for the struct
+// [AppProvidersResponse]
+type appProvidersResponseJSON struct {
+ Default apijson.Field
+ Providers apijson.Field
+ raw string
+ ExtraFields map[string]apijson.Field
+}
+
+func (r *AppProvidersResponse) UnmarshalJSON(data []byte) (err error) {
+ return apijson.UnmarshalRoot(data, r)
+}
+
+func (r appProvidersResponseJSON) 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 8028f2c53..16bb8ff88 100644
--- a/packages/tui/sdk/app_test.go
+++ b/packages/tui/sdk/app_test.go
@@ -107,3 +107,25 @@ func TestAppModes(t *testing.T) {
t.Fatalf("err should be nil: %s", err.Error())
}
}
+
+func TestAppProviders(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.Providers(context.TODO())
+ if err != nil {
+ var apierr *opencode.Error
+ if errors.As(err, &apierr) {
+ t.Log(string(apierr.DumpRequest(true)))
+ }
+ t.Fatalf("err should be nil: %s", err.Error())
+ }
+}
diff --git a/packages/tui/sdk/config.go b/packages/tui/sdk/config.go
index 4f5bf8a07..cf89e2d9f 100644
--- a/packages/tui/sdk/config.go
+++ b/packages/tui/sdk/config.go
@@ -40,14 +40,6 @@ func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (
return
}
-// List all providers
-func (r *ConfigService) Providers(ctx context.Context, opts ...option.RequestOption) (res *ConfigProvidersResponse, err error) {
- opts = append(r.Options[:], opts...)
- path := "config/providers"
- err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...)
- return
-}
-
type Config struct {
// JSON schema reference for configuration validation
Schema string `json:"$schema"`
@@ -62,12 +54,15 @@ type Config struct {
// Additional instruction files or patterns to include
Instructions []string `json:"instructions"`
// Custom keybind configurations
- Keybinds Keybinds `json:"keybinds"`
+ Keybinds KeybindsConfig `json:"keybinds"`
+ // Layout to use for the TUI
+ Layout LayoutConfig `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"`
- Mode ConfigMode `json:"mode"`
+ Mcp map[string]ConfigMcp `json:"mcp"`
+ // Modes configuration, see https://opencode.ai/docs/modes
+ 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
@@ -91,6 +86,7 @@ type configJSON struct {
Experimental apijson.Field
Instructions apijson.Field
Keybinds apijson.Field
+ Layout apijson.Field
LogLevel apijson.Field
Mcp apijson.Field
Mode apijson.Field
@@ -243,12 +239,12 @@ func (r *ConfigMcp) UnmarshalJSON(data []byte) (err error) {
// AsUnion returns a [ConfigMcpUnion] interface which you can cast to the specific
// types for more type safety.
//
-// Possible runtime types of the union are [McpLocal], [McpRemote].
+// Possible runtime types of the union are [McpLocalConfig], [McpRemoteConfig].
func (r ConfigMcp) AsUnion() ConfigMcpUnion {
return r.union
}
-// Union satisfied by [McpLocal] or [McpRemote].
+// Union satisfied by [McpLocalConfig] or [McpRemoteConfig].
type ConfigMcpUnion interface {
implementsConfigMcp()
}
@@ -259,12 +255,12 @@ func init() {
"type",
apijson.UnionVariant{
TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(McpLocal{}),
+ Type: reflect.TypeOf(McpLocalConfig{}),
DiscriminatorValue: "local",
},
apijson.UnionVariant{
TypeFilter: gjson.JSON,
- Type: reflect.TypeOf(McpRemote{}),
+ Type: reflect.TypeOf(McpRemoteConfig{}),
DiscriminatorValue: "remote",
},
)
@@ -286,10 +282,11 @@ func (r ConfigMcpType) IsKnown() bool {
return false
}
+// Modes configuration, see https://opencode.ai/docs/modes
type ConfigMode struct {
- Build ConfigModeBuild `json:"build"`
- Plan ConfigModePlan `json:"plan"`
- ExtraFields map[string]ConfigMode `json:"-,extras"`
+ Build ModeConfig `json:"build"`
+ Plan ModeConfig `json:"plan"`
+ ExtraFields map[string]ModeConfig `json:"-,extras"`
JSON configModeJSON `json:"-"`
}
@@ -309,54 +306,6 @@ 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"`
@@ -495,7 +444,7 @@ func (r ConfigShare) IsKnown() bool {
return false
}
-type Keybinds struct {
+type KeybindsConfig struct {
// Exit the application
AppExit string `json:"app_exit,required"`
// Show help dialog
@@ -548,6 +497,8 @@ type Keybinds struct {
ProjectInit string `json:"project_init,required"`
// Compact the session
SessionCompact string `json:"session_compact,required"`
+ // Export session to editor
+ SessionExport string `json:"session_export,required"`
// Interrupt current session
SessionInterrupt string `json:"session_interrupt,required"`
// List all sessions
@@ -565,12 +516,12 @@ type Keybinds struct {
// List available themes
ThemeList string `json:"theme_list,required"`
// Toggle tool details
- ToolDetails string `json:"tool_details,required"`
- JSON keybindsJSON `json:"-"`
+ ToolDetails string `json:"tool_details,required"`
+ JSON keybindsConfigJSON `json:"-"`
}
-// keybindsJSON contains the JSON metadata for the struct [Keybinds]
-type keybindsJSON struct {
+// keybindsConfigJSON contains the JSON metadata for the struct [KeybindsConfig]
+type keybindsConfigJSON struct {
AppExit apijson.Field
AppHelp apijson.Field
EditorOpen apijson.Field
@@ -597,6 +548,7 @@ type keybindsJSON struct {
ModelList apijson.Field
ProjectInit apijson.Field
SessionCompact apijson.Field
+ SessionExport apijson.Field
SessionInterrupt apijson.Field
SessionList apijson.Field
SessionNew apijson.Field
@@ -610,28 +562,43 @@ type keybindsJSON struct {
ExtraFields map[string]apijson.Field
}
-func (r *Keybinds) UnmarshalJSON(data []byte) (err error) {
+func (r *KeybindsConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
-func (r keybindsJSON) RawJSON() string {
+func (r keybindsConfigJSON) RawJSON() string {
return r.raw
}
-type McpLocal struct {
+type LayoutConfig string
+
+const (
+ LayoutConfigAuto LayoutConfig = "auto"
+ LayoutConfigStretch LayoutConfig = "stretch"
+)
+
+func (r LayoutConfig) IsKnown() bool {
+ switch r {
+ case LayoutConfigAuto, LayoutConfigStretch:
+ return true
+ }
+ return false
+}
+
+type McpLocalConfig struct {
// Command and arguments to run the MCP server
Command []string `json:"command,required"`
// Type of MCP server connection
- Type McpLocalType `json:"type,required"`
+ Type McpLocalConfigType `json:"type,required"`
// Enable or disable the MCP server on startup
Enabled bool `json:"enabled"`
// Environment variables to set when running the MCP server
- Environment map[string]string `json:"environment"`
- JSON mcpLocalJSON `json:"-"`
+ Environment map[string]string `json:"environment"`
+ JSON mcpLocalConfigJSON `json:"-"`
}
-// mcpLocalJSON contains the JSON metadata for the struct [McpLocal]
-type mcpLocalJSON struct {
+// mcpLocalConfigJSON contains the JSON metadata for the struct [McpLocalConfig]
+type mcpLocalConfigJSON struct {
Command apijson.Field
Type apijson.Field
Enabled apijson.Field
@@ -640,43 +607,43 @@ type mcpLocalJSON struct {
ExtraFields map[string]apijson.Field
}
-func (r *McpLocal) UnmarshalJSON(data []byte) (err error) {
+func (r *McpLocalConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
-func (r mcpLocalJSON) RawJSON() string {
+func (r mcpLocalConfigJSON) RawJSON() string {
return r.raw
}
-func (r McpLocal) implementsConfigMcp() {}
+func (r McpLocalConfig) implementsConfigMcp() {}
// Type of MCP server connection
-type McpLocalType string
+type McpLocalConfigType string
const (
- McpLocalTypeLocal McpLocalType = "local"
+ McpLocalConfigTypeLocal McpLocalConfigType = "local"
)
-func (r McpLocalType) IsKnown() bool {
+func (r McpLocalConfigType) IsKnown() bool {
switch r {
- case McpLocalTypeLocal:
+ case McpLocalConfigTypeLocal:
return true
}
return false
}
-type McpRemote struct {
+type McpRemoteConfig struct {
// Type of MCP server connection
- Type McpRemoteType `json:"type,required"`
+ Type McpRemoteConfigType `json:"type,required"`
// URL of the remote MCP server
URL string `json:"url,required"`
// Enable or disable the MCP server on startup
- Enabled bool `json:"enabled"`
- JSON mcpRemoteJSON `json:"-"`
+ Enabled bool `json:"enabled"`
+ JSON mcpRemoteConfigJSON `json:"-"`
}
-// mcpRemoteJSON contains the JSON metadata for the struct [McpRemote]
-type mcpRemoteJSON struct {
+// mcpRemoteConfigJSON contains the JSON metadata for the struct [McpRemoteConfig]
+type mcpRemoteConfigJSON struct {
Type apijson.Field
URL apijson.Field
Enabled apijson.Field
@@ -684,166 +651,51 @@ type mcpRemoteJSON struct {
ExtraFields map[string]apijson.Field
}
-func (r *McpRemote) UnmarshalJSON(data []byte) (err error) {
+func (r *McpRemoteConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
-func (r mcpRemoteJSON) RawJSON() string {
+func (r mcpRemoteConfigJSON) RawJSON() string {
return r.raw
}
-func (r McpRemote) implementsConfigMcp() {}
+func (r McpRemoteConfig) implementsConfigMcp() {}
// Type of MCP server connection
-type McpRemoteType string
+type McpRemoteConfigType string
const (
- McpRemoteTypeRemote McpRemoteType = "remote"
+ McpRemoteConfigTypeRemote McpRemoteConfigType = "remote"
)
-func (r McpRemoteType) IsKnown() bool {
+func (r McpRemoteConfigType) IsKnown() bool {
switch r {
- case McpRemoteTypeRemote:
+ case McpRemoteConfigTypeRemote:
return true
}
return false
}
-type Model struct {
- ID string `json:"id,required"`
- Attachment bool `json:"attachment,required"`
- Cost ModelCost `json:"cost,required"`
- Limit ModelLimit `json:"limit,required"`
- Name string `json:"name,required"`
- Options map[string]interface{} `json:"options,required"`
- Reasoning bool `json:"reasoning,required"`
- ReleaseDate string `json:"release_date,required"`
- Temperature bool `json:"temperature,required"`
- ToolCall bool `json:"tool_call,required"`
- JSON modelJSON `json:"-"`
-}
-
-// modelJSON contains the JSON metadata for the struct [Model]
-type modelJSON struct {
- ID apijson.Field
- Attachment apijson.Field
- Cost apijson.Field
- Limit apijson.Field
- Name apijson.Field
- Options apijson.Field
- Reasoning apijson.Field
- ReleaseDate apijson.Field
- Temperature apijson.Field
- ToolCall apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r *Model) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r modelJSON) RawJSON() string {
- return r.raw
-}
-
-type ModelCost struct {
- Input float64 `json:"input,required"`
- Output float64 `json:"output,required"`
- CacheRead float64 `json:"cache_read"`
- CacheWrite float64 `json:"cache_write"`
- JSON modelCostJSON `json:"-"`
-}
-
-// modelCostJSON contains the JSON metadata for the struct [ModelCost]
-type modelCostJSON struct {
- Input apijson.Field
- Output apijson.Field
- CacheRead apijson.Field
- CacheWrite apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r *ModelCost) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r modelCostJSON) RawJSON() string {
- return r.raw
-}
-
-type ModelLimit struct {
- Context float64 `json:"context,required"`
- Output float64 `json:"output,required"`
- JSON modelLimitJSON `json:"-"`
-}
-
-// modelLimitJSON contains the JSON metadata for the struct [ModelLimit]
-type modelLimitJSON struct {
- Context apijson.Field
- Output apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
+type ModeConfig struct {
+ Model string `json:"model"`
+ Prompt string `json:"prompt"`
+ Tools map[string]bool `json:"tools"`
+ JSON modeConfigJSON `json:"-"`
}
-func (r *ModelLimit) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r modelLimitJSON) RawJSON() string {
- return r.raw
-}
-
-type Provider struct {
- ID string `json:"id,required"`
- Env []string `json:"env,required"`
- Models map[string]Model `json:"models,required"`
- Name string `json:"name,required"`
- API string `json:"api"`
- Npm string `json:"npm"`
- JSON providerJSON `json:"-"`
-}
-
-// providerJSON contains the JSON metadata for the struct [Provider]
-type providerJSON struct {
- ID apijson.Field
- Env apijson.Field
- Models apijson.Field
- Name apijson.Field
- API apijson.Field
- Npm apijson.Field
- raw string
- ExtraFields map[string]apijson.Field
-}
-
-func (r *Provider) UnmarshalJSON(data []byte) (err error) {
- return apijson.UnmarshalRoot(data, r)
-}
-
-func (r providerJSON) RawJSON() string {
- return r.raw
-}
-
-type ConfigProvidersResponse struct {
- Default map[string]string `json:"default,required"`
- Providers []Provider `json:"providers,required"`
- JSON configProvidersResponseJSON `json:"-"`
-}
-
-// configProvidersResponseJSON contains the JSON metadata for the struct
-// [ConfigProvidersResponse]
-type configProvidersResponseJSON struct {
- Default apijson.Field
- Providers apijson.Field
+// modeConfigJSON contains the JSON metadata for the struct [ModeConfig]
+type modeConfigJSON struct {
+ Model apijson.Field
+ Prompt apijson.Field
+ Tools apijson.Field
raw string
ExtraFields map[string]apijson.Field
}
-func (r *ConfigProvidersResponse) UnmarshalJSON(data []byte) (err error) {
+func (r *ModeConfig) UnmarshalJSON(data []byte) (err error) {
return apijson.UnmarshalRoot(data, r)
}
-func (r configProvidersResponseJSON) RawJSON() string {
+func (r modeConfigJSON) RawJSON() string {
return r.raw
}
diff --git a/packages/tui/sdk/config_test.go b/packages/tui/sdk/config_test.go
index 57a1d158d..86e058a9a 100644
--- a/packages/tui/sdk/config_test.go
+++ b/packages/tui/sdk/config_test.go
@@ -34,25 +34,3 @@ func TestConfigGet(t *testing.T) {
t.Fatalf("err should be nil: %s", err.Error())
}
}
-
-func TestConfigProviders(t *testing.T) {
- t.Skip("skipped: tests are disabled for the time being")
- baseURL := "http://localhost:4010"
- if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok {
- baseURL = envURL
- }
- if !testutil.CheckTestServer(t, baseURL) {
- return
- }
- client := opencode.NewClient(
- option.WithBaseURL(baseURL),
- )
- _, err := client.Config.Providers(context.TODO())
- if err != nil {
- var apierr *opencode.Error
- if errors.As(err, &apierr) {
- t.Log(string(apierr.DumpRequest(true)))
- }
- t.Fatalf("err should be nil: %s", err.Error())
- }
-}
diff --git a/packages/web/src/content/docs/docs/troubleshooting.mdx b/packages/web/src/content/docs/docs/troubleshooting.mdx
index bb4f906ee..72064155e 100644
--- a/packages/web/src/content/docs/docs/troubleshooting.mdx
+++ b/packages/web/src/content/docs/docs/troubleshooting.mdx
@@ -116,3 +116,20 @@ export DISPLAY=:99.0
```
opencode will detect if you're using Wayland and prefer `wl-clipboard`, otherwise it will try to find clipboard tools in order of: `xclip` and `xsel`.
+
+---
+
+### TUI not rendering full width
+
+By default, opencode's TUI uses an "auto" layout that centers content with padding. If you want the TUI to use the full width of your terminal, you can configure the layout setting:
+
+```json
+{
+ "layout": "stretch"
+}
+```
+
+Add this to your `opencode.json` configuration file. The available layout options are:
+
+- `"auto"` (default) - Centers content with padding
+- `"stretch"` - Uses full terminal width