Browse Source

feat(tui): layout config to render full width

adamdotdevin 7 months ago
parent
commit
cdc1d8a94d

+ 8 - 1
packages/opencode/src/config/config.ts

@@ -125,6 +125,11 @@ export namespace Config {
       ref: "KeybindsConfig",
       ref: "KeybindsConfig",
     })
     })
 
 
+  export const Layout = z.enum(["auto", "stretch"]).openapi({
+    ref: "LayoutConfig",
+  })
+  export type Layout = z.infer<typeof Layout>
+
   export const Info = z
   export const Info = z
     .object({
     .object({
       $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
       $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -151,7 +156,8 @@ export namespace Config {
           plan: Mode.optional(),
           plan: Mode.optional(),
         })
         })
         .catchall(Mode)
         .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"),
       log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
       provider: z
       provider: z
         .record(
         .record(
@@ -164,6 +170,7 @@ export namespace Config {
         .describe("Custom provider configurations and model overrides"),
         .describe("Custom provider configurations and model overrides"),
       mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
       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"),
       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
       experimental: z
         .object({
         .object({
           hook: z
           hook: z

+ 3 - 3
packages/tui/internal/app/app.go

@@ -260,7 +260,7 @@ func (a *App) SwitchModeReverse() (*App, tea.Cmd) {
 }
 }
 
 
 func (a *App) InitializeProvider() 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 {
 	if err != nil {
 		slog.Error("Failed to list providers", "error", err)
 		slog.Error("Failed to list providers", "error", err)
 		// TODO: notify user
 		// TODO: notify user
@@ -355,7 +355,7 @@ func (a *App) InitializeProvider() tea.Cmd {
 }
 }
 
 
 func getDefaultModel(
 func getDefaultModel(
-	response *opencode.ConfigProvidersResponse,
+	response *opencode.AppProvidersResponse,
 	provider opencode.Provider,
 	provider opencode.Provider,
 ) *opencode.Model {
 ) *opencode.Model {
 	if match, ok := response.Default[provider.ID]; ok {
 	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) {
 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 {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 4 - 0
packages/tui/internal/app/constants.go

@@ -0,0 +1,4 @@
+package app
+
+const MAX_CONTAINER_WIDTH = 86
+const EDIT_DIFF_MAX_WIDTH = 180

+ 6 - 6
packages/tui/internal/commands/command.go

@@ -230,12 +230,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry {
 			Keybindings: parseBindings("<leader>t"),
 			Keybindings: parseBindings("<leader>t"),
 			Trigger:     []string{"themes"},
 			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,
 			Name:        FileCloseCommand,
 			Description: "close file",
 			Description: "close file",

+ 16 - 9
packages/tui/internal/components/chat/editor.go

@@ -27,8 +27,8 @@ import (
 
 
 type EditorComponent interface {
 type EditorComponent interface {
 	tea.Model
 	tea.Model
-	View(width int) string
-	Content(width int) string
+	tea.ViewModel
+	Content() string
 	Lines() int
 	Lines() int
 	Value() string
 	Value() string
 	Length() int
 	Length() int
@@ -46,6 +46,7 @@ type EditorComponent interface {
 
 
 type editorComponent struct {
 type editorComponent struct {
 	app                    *app.App
 	app                    *app.App
+	width                  int
 	textarea               textarea.Model
 	textarea               textarea.Model
 	spinner                spinner.Model
 	spinner                spinner.Model
 	interruptKeyInDebounce bool
 	interruptKeyInDebounce bool
@@ -61,6 +62,12 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmd tea.Cmd
 	var cmd tea.Cmd
 
 
 	switch msg := msg.(type) {
 	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:
 	case spinner.TickMsg:
 		m.spinner, cmd = m.spinner.Update(msg)
 		m.spinner, cmd = m.spinner.Update(msg)
 		return m, cmd
 		return m, cmd
@@ -227,7 +234,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return m, tea.Batch(cmds...)
 	return m, tea.Batch(cmds...)
 }
 }
 
 
-func (m *editorComponent) Content(width int) string {
+func (m *editorComponent) Content() string {
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 	base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
 	base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
 	muted := styles.NewStyle().Foreground(t.TextMuted()).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)
 		Bold(true)
 	prompt := promptStyle.Render(">")
 	prompt := promptStyle.Render(">")
 
 
-	m.textarea.SetWidth(width - 6)
+	m.textarea.SetWidth(m.width - 6)
 	textarea := lipgloss.JoinHorizontal(
 	textarea := lipgloss.JoinHorizontal(
 		lipgloss.Top,
 		lipgloss.Top,
 		prompt,
 		prompt,
@@ -248,7 +255,7 @@ func (m *editorComponent) Content(width int) string {
 	}
 	}
 	textarea = styles.NewStyle().
 	textarea = styles.NewStyle().
 		Background(t.BackgroundElement()).
 		Background(t.BackgroundElement()).
-		Width(width).
+		Width(m.width).
 		PaddingTop(1).
 		PaddingTop(1).
 		PaddingBottom(1).
 		PaddingBottom(1).
 		BorderStyle(lipgloss.ThickBorder()).
 		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)
 		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("")
 	spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
 
 
 	info := hint + spacer + model
 	info := hint + spacer + model
@@ -294,10 +301,10 @@ func (m *editorComponent) Content(width int) string {
 	return content
 	return content
 }
 }
 
 
-func (m *editorComponent) View(width int) string {
+func (m *editorComponent) View() string {
 	if m.Lines() > 1 {
 	if m.Lines() > 1 {
 		return lipgloss.Place(
 		return lipgloss.Place(
-			width,
+			m.width,
 			5,
 			5,
 			lipgloss.Center,
 			lipgloss.Center,
 			lipgloss.Center,
 			lipgloss.Center,
@@ -305,7 +312,7 @@ func (m *editorComponent) View(width int) string {
 			styles.WhitespaceStyle(theme.CurrentTheme().Background()),
 			styles.WhitespaceStyle(theme.CurrentTheme().Background()),
 		)
 		)
 	}
 	}
-	return m.Content(width)
+	return m.Content()
 }
 }
 
 
 func (m *editorComponent) Focused() bool {
 func (m *editorComponent) Focused() bool {

+ 16 - 5
packages/tui/internal/components/chat/message.go

@@ -10,6 +10,7 @@ import (
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/lipgloss/v2"
 	"github.com/charmbracelet/lipgloss/v2/compat"
 	"github.com/charmbracelet/lipgloss/v2/compat"
 	"github.com/charmbracelet/x/ansi"
 	"github.com/charmbracelet/x/ansi"
+	"github.com/muesli/reflow/truncate"
 	"github.com/sst/opencode-sdk-go"
 	"github.com/sst/opencode-sdk-go"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/app"
 	"github.com/sst/opencode/internal/components/diff"
 	"github.com/sst/opencode/internal/components/diff"
@@ -319,11 +320,19 @@ func renderToolDetails(
 				if diffField != nil {
 				if diffField != nil {
 					patch := diffField.(string)
 					patch := diffField.(string)
 					var formattedDiff 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)
 					body = strings.TrimSpace(formattedDiff)
 					style := styles.NewStyle().
 					style := styles.NewStyle().
 						Background(backgroundColor).
 						Background(backgroundColor).
@@ -551,6 +560,8 @@ func renderToolTitle(
 		toolName := renderToolName(toolCall.Tool)
 		toolName := renderToolName(toolCall.Tool)
 		title = fmt.Sprintf("%s %s", toolName, toolArgs)
 		title = fmt.Sprintf("%s %s", toolName, toolArgs)
 	}
 	}
+
+	title = truncate.StringWithTail(title, uint(width-6), "...")
 	return title
 	return title
 }
 }
 
 

+ 83 - 39
packages/tui/internal/components/chat/messages.go

@@ -19,8 +19,7 @@ import (
 
 
 type MessagesComponent interface {
 type MessagesComponent interface {
 	tea.Model
 	tea.Model
-	View(width, height int) string
-	SetWidth(width int) tea.Cmd
+	tea.ViewModel
 	PageUp() (tea.Model, tea.Cmd)
 	PageUp() (tea.Model, tea.Cmd)
 	PageDown() (tea.Model, tea.Cmd)
 	PageDown() (tea.Model, tea.Cmd)
 	HalfPageUp() (tea.Model, tea.Cmd)
 	HalfPageUp() (tea.Model, tea.Cmd)
@@ -32,8 +31,9 @@ type MessagesComponent interface {
 }
 }
 
 
 type messagesComponent struct {
 type messagesComponent struct {
-	width           int
+	width, height   int
 	app             *app.App
 	app             *app.App
+	header          string
 	viewport        viewport.Model
 	viewport        viewport.Model
 	cache           *PartCache
 	cache           *PartCache
 	rendering       bool
 	rendering       bool
@@ -53,6 +53,17 @@ func (m *messagesComponent) Init() tea.Cmd {
 func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	var cmds []tea.Cmd
 	switch msg := msg.(type) {
 	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:
 	case app.SendMsg:
 		m.viewport.GotoBottom()
 		m.viewport.GotoBottom()
 		m.tail = true
 		m.tail = true
@@ -82,21 +93,18 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 
 	case opencode.EventListResponseEventSessionUpdated:
 	case opencode.EventListResponseEventSessionUpdated:
 		if msg.Properties.Info.ID == m.app.Session.ID {
 		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:
 	case opencode.EventListResponseEventMessageUpdated:
 		if msg.Properties.Info.SessionID == m.app.Session.ID {
 		if msg.Properties.Info.SessionID == m.app.Session.ID {
-			m.renderView(m.width)
+			m.renderView()
 			if m.tail {
 			if m.tail {
 				m.viewport.GotoBottom()
 				m.viewport.GotoBottom()
 			}
 			}
 		}
 		}
 	case opencode.EventListResponseEventMessagePartUpdated:
 	case opencode.EventListResponseEventMessagePartUpdated:
 		if msg.Properties.Part.SessionID == m.app.Session.ID {
 		if msg.Properties.Part.SessionID == m.app.Session.ID {
-			m.renderView(m.width)
+			m.renderView()
 			if m.tail {
 			if m.tail {
 				m.viewport.GotoBottom()
 				m.viewport.GotoBottom()
 			}
 			}
@@ -111,10 +119,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return m, tea.Batch(cmds...)
 	return m, tea.Batch(cmds...)
 }
 }
 
 
-func (m *messagesComponent) renderView(width int) {
+func (m *messagesComponent) renderView() {
 	measure := util.Measure("messages.renderView")
 	measure := util.Measure("messages.renderView")
 	defer measure("messageCount", len(m.app.Messages))
 	defer measure("messageCount", len(m.app.Messages))
 
 
+	m.header = m.renderHeader()
+
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 	blocks := make([]string, 0)
 	blocks := make([]string, 0)
 	m.partCount = 0
 	m.partCount = 0
@@ -122,6 +132,11 @@ func (m *messagesComponent) renderView(width int) {
 
 
 	orphanedToolCalls := make([]opencode.ToolPart, 0)
 	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 {
 	for _, message := range m.app.Messages {
 		var content string
 		var content string
 		var cached bool
 		var cached bool
@@ -185,6 +200,12 @@ func (m *messagesComponent) renderView(width int) {
 							width,
 							width,
 							files,
 							files,
 						)
 						)
+						content = lipgloss.PlaceHorizontal(
+							m.width,
+							lipgloss.Center,
+							content,
+							styles.WhitespaceStyle(t.Background()),
+						)
 						m.cache.Set(key, content)
 						m.cache.Set(key, content)
 					}
 					}
 					if content != "" {
 					if content != "" {
@@ -246,6 +267,12 @@ func (m *messagesComponent) renderView(width int) {
 								"",
 								"",
 								toolCallParts...,
 								toolCallParts...,
 							)
 							)
+							content = lipgloss.PlaceHorizontal(
+								m.width,
+								lipgloss.Center,
+								content,
+								styles.WhitespaceStyle(t.Background()),
+							)
 							m.cache.Set(key, content)
 							m.cache.Set(key, content)
 						}
 						}
 					} else {
 					} else {
@@ -259,6 +286,12 @@ func (m *messagesComponent) renderView(width int) {
 							"",
 							"",
 							toolCallParts...,
 							toolCallParts...,
 						)
 						)
+						content = lipgloss.PlaceHorizontal(
+							m.width,
+							lipgloss.Center,
+							content,
+							styles.WhitespaceStyle(t.Background()),
+						)
 					}
 					}
 					if content != "" {
 					if content != "" {
 						m.partCount++
 						m.partCount++
@@ -273,6 +306,13 @@ func (m *messagesComponent) renderView(width int) {
 						continue
 						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 {
 					if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
 						key := m.cache.GenerateKey(casted.ID,
 						key := m.cache.GenerateKey(casted.ID,
 							part.ID,
 							part.ID,
@@ -286,6 +326,12 @@ func (m *messagesComponent) renderView(width int) {
 								part,
 								part,
 								width,
 								width,
 							)
 							)
+							content = lipgloss.PlaceHorizontal(
+								m.width,
+								lipgloss.Center,
+								content,
+								styles.WhitespaceStyle(t.Background()),
+							)
 							m.cache.Set(key, content)
 							m.cache.Set(key, content)
 						}
 						}
 					} else {
 					} else {
@@ -295,6 +341,12 @@ func (m *messagesComponent) renderView(width int) {
 							part,
 							part,
 							width,
 							width,
 						)
 						)
+						content = lipgloss.PlaceHorizontal(
+							m.width,
+							lipgloss.Center,
+							content,
+							styles.WhitespaceStyle(t.Background()),
+						)
 					}
 					}
 					if content != "" {
 					if content != "" {
 						m.partCount++
 						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"))
 	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 == "" {
 	if m.app.Session.ID == "" {
 		return ""
 		return ""
 	}
 	}
 
 
+	headerWidth := min(m.width, app.MAX_CONTAINER_WIDTH)
+	if m.app.Config.Layout == opencode.LayoutConfigStretch {
+		headerWidth = m.width
+	}
+
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 	base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
 	base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
 	muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
 	muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
 	headerLines := []string{}
 	headerLines := []string{}
 	headerLines = append(
 	headerLines = append(
 		headerLines,
 		headerLines,
-		util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
+		util.ToMarkdown("# "+m.app.Session.Title, headerWidth-6, t.Background()),
 	)
 	)
 
 
 	share := ""
 	share := ""
@@ -397,7 +454,7 @@ func (m *messagesComponent) header(width int) string {
 			Direction:  layout.Row,
 			Direction:  layout.Row,
 			Justify:    layout.JustifySpaceBetween,
 			Justify:    layout.JustifySpaceBetween,
 			Align:      layout.AlignStretch,
 			Align:      layout.AlignStretch,
-			Width:      width - 6,
+			Width:      headerWidth - 6,
 		},
 		},
 		layout.FlexItem{
 		layout.FlexItem{
 			View: share,
 			View: share,
@@ -408,12 +465,10 @@ func (m *messagesComponent) header(width int) string {
 	)
 	)
 
 
 	headerLines = append(headerLines, share)
 	headerLines = append(headerLines, share)
-
 	header := strings.Join(headerLines, "\n")
 	header := strings.Join(headerLines, "\n")
-
 	header = styles.NewStyle().
 	header = styles.NewStyle().
 		Background(t.Background()).
 		Background(t.Background()).
-		Width(width).
+		Width(headerWidth).
 		PaddingLeft(2).
 		PaddingLeft(2).
 		PaddingRight(2).
 		PaddingRight(2).
 		BorderLeft(true).
 		BorderLeft(true).
@@ -422,6 +477,12 @@ func (m *messagesComponent) header(width int) string {
 		BorderForeground(t.BackgroundElement()).
 		BorderForeground(t.BackgroundElement()).
 		BorderStyle(lipgloss.ThickBorder()).
 		BorderStyle(lipgloss.ThickBorder()).
 		Render(header)
 		Render(header)
+	header = lipgloss.PlaceHorizontal(
+		m.width,
+		lipgloss.Center,
+		header,
+		styles.WhitespaceStyle(t.Background()),
+	)
 
 
 	return "\n" + header + "\n"
 	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()
 	t := theme.CurrentTheme()
 	if m.rendering {
 	if m.rendering {
 		return lipgloss.Place(
 		return lipgloss.Place(
-			width,
-			height,
+			m.width,
+			m.height,
 			lipgloss.Center,
 			lipgloss.Center,
 			lipgloss.Center,
 			lipgloss.Center,
 			styles.NewStyle().Background(t.Background()).Render(""),
 			styles.NewStyle().Background(t.Background()).Render(""),
 			styles.WhitespaceStyle(t.Background()),
 			styles.WhitespaceStyle(t.Background()),
 		)
 		)
 	}
 	}
-	header := m.header(width)
-	m.viewport.SetWidth(width)
-	m.viewport.SetHeight(height - lipgloss.Height(header))
 
 
 	return styles.NewStyle().
 	return styles.NewStyle().
 		Background(t.Background()).
 		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 {
 func (m *messagesComponent) Reload() tea.Cmd {
 	return func() tea.Msg {
 	return func() tea.Msg {
-		m.renderView(m.width)
+		m.renderView()
 		return renderFinishedMsg{}
 		return renderFinishedMsg{}
 	}
 	}
 }
 }

+ 44 - 105
packages/tui/internal/tui/tui.go

@@ -56,7 +56,6 @@ const (
 
 
 const interruptDebounceTimeout = 1 * time.Second
 const interruptDebounceTimeout = 1 * time.Second
 const exitDebounceTimeout = 1 * time.Second
 const exitDebounceTimeout = 1 * time.Second
-const fileViewerFullWidthCutoff = 160
 
 
 type appModel struct {
 type appModel struct {
 	width, height        int
 	width, height        int
@@ -77,10 +76,6 @@ type appModel struct {
 	exitKeyState      ExitKeyState
 	exitKeyState      ExitKeyState
 	messagesRight     bool
 	messagesRight     bool
 	fileViewer        fileviewer.Model
 	fileViewer        fileviewer.Model
-	lastMouse         tea.Mouse
-	fileViewerStart   int
-	fileViewerEnd     int
-	fileViewerHit     bool
 }
 }
 
 
 func (a appModel) Init() tea.Cmd {
 func (a appModel) Init() tea.Cmd {
@@ -288,30 +283,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return a, cmd
 		return a, cmd
 	case tea.MouseWheelMsg:
 	case tea.MouseWheelMsg:
 		if a.modal != nil {
 		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)
 			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...)
 		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:
 	case tea.BackgroundColorMsg:
 		styles.Terminal = &styles.TerminalInfo{
 		styles.Terminal = &styles.TerminalInfo{
 			Background:       msg.Color,
 			Background:       msg.Color,
@@ -458,14 +439,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tea.WindowSizeMsg:
 	case tea.WindowSizeMsg:
 		msg.Height -= 2 // Make space for the status bar
 		msg.Height -= 2 // Make space for the status bar
 		a.width, a.height = msg.Width, msg.Height
 		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{
 		layout.Current = &layout.LayoutInfo{
 			Viewport: layout.Dimensions{
 			Viewport: layout.Dimensions{
 				Width:  a.width,
 				Width:  a.width,
@@ -475,21 +449,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				Width: container,
 				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:
 	case app.SessionSelectedMsg:
 		messages, err := a.app.ListMessages(context.Background(), msg.ID)
 		messages, err := a.app.ListMessages(context.Background(), msg.ID)
 		if err != nil {
 		if err != nil {
@@ -569,48 +528,22 @@ func (a appModel) View() string {
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
 
 
 	var mainLayout string
 	var mainLayout string
-	mainWidth := layout.Current.Container.Width - 4
+
 	if a.app.Session.ID == "" {
 	if a.app.Session.ID == "" {
-		mainLayout = a.home(mainWidth)
+		mainLayout = a.home()
 	} else {
 	} else {
-		mainLayout = a.chat(mainWidth)
+		mainLayout = a.chat()
 	}
 	}
 	mainLayout = styles.NewStyle().
 	mainLayout = styles.NewStyle().
 		Background(t.Background()).
 		Background(t.Background()).
 		Padding(0, 2).
 		Padding(0, 2).
 		Render(mainLayout)
 		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())
 	mainStyle := styles.NewStyle().Background(t.Background())
 	mainLayout = mainStyle.Render(mainLayout)
 	mainLayout = mainStyle.Render(mainLayout)
@@ -646,8 +579,9 @@ func (a appModel) openFile(filepath string) (tea.Model, tea.Cmd) {
 	return a, cmd
 	return a, cmd
 }
 }
 
 
-func (a appModel) home(width int) string {
+func (a appModel) home() string {
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
+	effectiveWidth := a.width - 4
 	baseStyle := styles.NewStyle().Background(t.Background())
 	baseStyle := styles.NewStyle().Background(t.Background())
 	base := baseStyle.Render
 	base := baseStyle.Render
 	muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).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 := strings.Join([]string{logo, version}, "\n")
 	logoAndVersion = lipgloss.PlaceHorizontal(
 	logoAndVersion = lipgloss.PlaceHorizontal(
-		width,
+		effectiveWidth,
 		lipgloss.Center,
 		lipgloss.Center,
 		logoAndVersion,
 		logoAndVersion,
 		styles.WhitespaceStyle(t.Background()),
 		styles.WhitespaceStyle(t.Background()),
@@ -689,7 +623,7 @@ func (a appModel) home(width int) string {
 		cmdcomp.WithLimit(6),
 		cmdcomp.WithLimit(6),
 	)
 	)
 	cmds := lipgloss.PlaceHorizontal(
 	cmds := lipgloss.PlaceHorizontal(
-		width,
+		effectiveWidth,
 		lipgloss.Center,
 		lipgloss.Center,
 		commandsView.View(),
 		commandsView.View(),
 		styles.WhitespaceStyle(t.Background()),
 		styles.WhitespaceStyle(t.Background()),
@@ -701,19 +635,16 @@ func (a appModel) home(width int) string {
 	lines = append(lines, logoAndVersion)
 	lines = append(lines, logoAndVersion)
 	lines = append(lines, "")
 	lines = append(lines, "")
 	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, cmds)
 	lines = append(lines, "")
 	lines = append(lines, "")
 	lines = append(lines, "")
 	lines = append(lines, "")
 
 
 	mainHeight := lipgloss.Height(strings.Join(lines, "\n"))
 	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(
 	editorView = lipgloss.PlaceHorizontal(
-		width,
+		effectiveWidth,
 		lipgloss.Center,
 		lipgloss.Center,
 		editorView,
 		editorView,
 		styles.WhitespaceStyle(t.Background()),
 		styles.WhitespaceStyle(t.Background()),
@@ -723,7 +654,7 @@ func (a appModel) home(width int) string {
 	editorLines := a.editor.Lines()
 	editorLines := a.editor.Lines()
 
 
 	mainLayout := lipgloss.Place(
 	mainLayout := lipgloss.Place(
-		width,
+		effectiveWidth,
 		a.height,
 		a.height,
 		lipgloss.Center,
 		lipgloss.Center,
 		lipgloss.Center,
 		lipgloss.Center,
@@ -731,14 +662,14 @@ func (a appModel) home(width int) string {
 		styles.WhitespaceStyle(t.Background()),
 		styles.WhitespaceStyle(t.Background()),
 	)
 	)
 
 
-	editorX := (width - editorWidth) / 2
+	editorX := (effectiveWidth - editorWidth) / 2
 	editorY := (a.height / 2) + (mainHeight / 2) - 2
 	editorY := (a.height / 2) + (mainHeight / 2) - 2
 
 
 	if editorLines > 1 {
 	if editorLines > 1 {
 		mainLayout = layout.PlaceOverlay(
 		mainLayout = layout.PlaceOverlay(
 			editorX,
 			editorX,
 			editorY,
 			editorY,
-			a.editor.Content(editorWidth),
+			a.editor.Content(),
 			mainLayout,
 			mainLayout,
 		)
 		)
 	}
 	}
@@ -759,23 +690,31 @@ func (a appModel) home(width int) string {
 	return mainLayout
 	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()
 	lines := a.editor.Lines()
-	messagesView := a.messages.View(width, a.height-5)
+	messagesView := a.messages.View()
 
 
 	editorWidth := lipgloss.Width(editorView)
 	editorWidth := lipgloss.Width(editorView)
 	editorHeight := max(lines, 5)
 	editorHeight := max(lines, 5)
+	editorView = lipgloss.PlaceHorizontal(
+		effectiveWidth,
+		lipgloss.Center,
+		editorView,
+		styles.WhitespaceStyle(t.Background()),
+	)
 
 
 	mainLayout := messagesView + "\n" + editorView
 	mainLayout := messagesView + "\n" + editorView
-	editorX := (a.width - editorWidth) / 2
+	editorX := (effectiveWidth - editorWidth) / 2
 
 
 	if lines > 1 {
 	if lines > 1 {
 		editorY := a.height - editorHeight
 		editorY := a.height - editorHeight
 		mainLayout = layout.PlaceOverlay(
 		mainLayout = layout.PlaceOverlay(
 			editorX,
 			editorX,
 			editorY,
 			editorY,
-			a.editor.Content(width),
+			a.editor.Content(),
 			mainLayout,
 			mainLayout,
 		)
 		)
 	}
 	}
@@ -968,11 +907,11 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
 	case commands.ThemeListCommand:
 	case commands.ThemeListCommand:
 		themeDialog := dialog.NewThemeDialog()
 		themeDialog := dialog.NewThemeDialog()
 		a.modal = themeDialog
 		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:
 	case commands.FileCloseCommand:
 		a.fileViewer, cmd = a.fileViewer.Clear()
 		a.fileViewer, cmd = a.fileViewer.Clear()
 		cmds = append(cmds, cmd)
 		cmds = append(cmds, cmd)

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

@@ -1,4 +1,4 @@
 configured_endpoints: 22
 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

+ 9 - 7
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#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#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#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:
 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 /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="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 /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
 # Find
 
 
@@ -59,17 +63,15 @@ Methods:
 Response Types:
 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#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:
 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">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
 # Session
 
 

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

@@ -63,6 +63,14 @@ func (r *AppService) Modes(ctx context.Context, opts ...option.RequestOption) (r
 	return
 	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 {
 type App struct {
 	Git      bool    `json:"git,required"`
 	Git      bool    `json:"git,required"`
 	Hostname string  `json:"hostname,required"`
 	Hostname string  `json:"hostname,required"`
@@ -203,6 +211,145 @@ func (r modeModelJSON) RawJSON() string {
 	return r.raw
 	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 {
 type AppLogParams struct {
 	// Log level
 	// Log level
 	Level param.Field[AppLogParamsLevel] `json:"level,required"`
 	Level param.Field[AppLogParamsLevel] `json:"level,required"`

+ 22 - 0
packages/tui/sdk/app_test.go

@@ -107,3 +107,25 @@ func TestAppModes(t *testing.T) {
 		t.Fatalf("err should be nil: %s", err.Error())
 		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())
+	}
+}

+ 78 - 226
packages/tui/sdk/config.go

@@ -40,14 +40,6 @@ func (r *ConfigService) Get(ctx context.Context, opts ...option.RequestOption) (
 	return
 	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 {
 type Config struct {
 	// JSON schema reference for configuration validation
 	// JSON schema reference for configuration validation
 	Schema string `json:"$schema"`
 	Schema string `json:"$schema"`
@@ -62,12 +54,15 @@ type Config struct {
 	// Additional instruction files or patterns to include
 	// Additional instruction files or patterns to include
 	Instructions []string `json:"instructions"`
 	Instructions []string `json:"instructions"`
 	// Custom keybind configurations
 	// 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
 	// Minimum log level to write to log files
 	LogLevel LogLevel `json:"log_level"`
 	LogLevel LogLevel `json:"log_level"`
 	// MCP (Model Context Protocol) server configurations
 	// 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 to use in the format of provider/model, eg anthropic/claude-2
 	Model string `json:"model"`
 	Model string `json:"model"`
 	// Custom provider configurations and model overrides
 	// Custom provider configurations and model overrides
@@ -91,6 +86,7 @@ type configJSON struct {
 	Experimental      apijson.Field
 	Experimental      apijson.Field
 	Instructions      apijson.Field
 	Instructions      apijson.Field
 	Keybinds          apijson.Field
 	Keybinds          apijson.Field
+	Layout            apijson.Field
 	LogLevel          apijson.Field
 	LogLevel          apijson.Field
 	Mcp               apijson.Field
 	Mcp               apijson.Field
 	Mode              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
 // AsUnion returns a [ConfigMcpUnion] interface which you can cast to the specific
 // types for more type safety.
 // 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 {
 func (r ConfigMcp) AsUnion() ConfigMcpUnion {
 	return r.union
 	return r.union
 }
 }
 
 
-// Union satisfied by [McpLocal] or [McpRemote].
+// Union satisfied by [McpLocalConfig] or [McpRemoteConfig].
 type ConfigMcpUnion interface {
 type ConfigMcpUnion interface {
 	implementsConfigMcp()
 	implementsConfigMcp()
 }
 }
@@ -259,12 +255,12 @@ func init() {
 		"type",
 		"type",
 		apijson.UnionVariant{
 		apijson.UnionVariant{
 			TypeFilter:         gjson.JSON,
 			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(McpLocal{}),
+			Type:               reflect.TypeOf(McpLocalConfig{}),
 			DiscriminatorValue: "local",
 			DiscriminatorValue: "local",
 		},
 		},
 		apijson.UnionVariant{
 		apijson.UnionVariant{
 			TypeFilter:         gjson.JSON,
 			TypeFilter:         gjson.JSON,
-			Type:               reflect.TypeOf(McpRemote{}),
+			Type:               reflect.TypeOf(McpRemoteConfig{}),
 			DiscriminatorValue: "remote",
 			DiscriminatorValue: "remote",
 		},
 		},
 	)
 	)
@@ -286,10 +282,11 @@ func (r ConfigMcpType) IsKnown() bool {
 	return false
 	return false
 }
 }
 
 
+// Modes configuration, see https://opencode.ai/docs/modes
 type ConfigMode struct {
 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:"-"`
 	JSON        configModeJSON        `json:"-"`
 }
 }
 
 
@@ -309,54 +306,6 @@ func (r configModeJSON) RawJSON() string {
 	return r.raw
 	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 {
 type ConfigProvider struct {
 	Models  map[string]ConfigProviderModel `json:"models,required"`
 	Models  map[string]ConfigProviderModel `json:"models,required"`
 	ID      string                         `json:"id"`
 	ID      string                         `json:"id"`
@@ -495,7 +444,7 @@ func (r ConfigShare) IsKnown() bool {
 	return false
 	return false
 }
 }
 
 
-type Keybinds struct {
+type KeybindsConfig struct {
 	// Exit the application
 	// Exit the application
 	AppExit string `json:"app_exit,required"`
 	AppExit string `json:"app_exit,required"`
 	// Show help dialog
 	// Show help dialog
@@ -548,6 +497,8 @@ type Keybinds struct {
 	ProjectInit string `json:"project_init,required"`
 	ProjectInit string `json:"project_init,required"`
 	// Compact the session
 	// Compact the session
 	SessionCompact string `json:"session_compact,required"`
 	SessionCompact string `json:"session_compact,required"`
+	// Export session to editor
+	SessionExport string `json:"session_export,required"`
 	// Interrupt current session
 	// Interrupt current session
 	SessionInterrupt string `json:"session_interrupt,required"`
 	SessionInterrupt string `json:"session_interrupt,required"`
 	// List all sessions
 	// List all sessions
@@ -565,12 +516,12 @@ type Keybinds struct {
 	// List available themes
 	// List available themes
 	ThemeList string `json:"theme_list,required"`
 	ThemeList string `json:"theme_list,required"`
 	// Toggle tool details
 	// 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
 	AppExit              apijson.Field
 	AppHelp              apijson.Field
 	AppHelp              apijson.Field
 	EditorOpen           apijson.Field
 	EditorOpen           apijson.Field
@@ -597,6 +548,7 @@ type keybindsJSON struct {
 	ModelList            apijson.Field
 	ModelList            apijson.Field
 	ProjectInit          apijson.Field
 	ProjectInit          apijson.Field
 	SessionCompact       apijson.Field
 	SessionCompact       apijson.Field
+	SessionExport        apijson.Field
 	SessionInterrupt     apijson.Field
 	SessionInterrupt     apijson.Field
 	SessionList          apijson.Field
 	SessionList          apijson.Field
 	SessionNew           apijson.Field
 	SessionNew           apijson.Field
@@ -610,28 +562,43 @@ type keybindsJSON struct {
 	ExtraFields          map[string]apijson.Field
 	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)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r keybindsJSON) RawJSON() string {
+func (r keybindsConfigJSON) RawJSON() string {
 	return r.raw
 	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 and arguments to run the MCP server
 	Command []string `json:"command,required"`
 	Command []string `json:"command,required"`
 	// Type of MCP server connection
 	// Type of MCP server connection
-	Type McpLocalType `json:"type,required"`
+	Type McpLocalConfigType `json:"type,required"`
 	// Enable or disable the MCP server on startup
 	// Enable or disable the MCP server on startup
 	Enabled bool `json:"enabled"`
 	Enabled bool `json:"enabled"`
 	// Environment variables to set when running the MCP server
 	// 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
 	Command     apijson.Field
 	Type        apijson.Field
 	Type        apijson.Field
 	Enabled     apijson.Field
 	Enabled     apijson.Field
@@ -640,43 +607,43 @@ type mcpLocalJSON struct {
 	ExtraFields map[string]apijson.Field
 	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)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r mcpLocalJSON) RawJSON() string {
+func (r mcpLocalConfigJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }
 
 
-func (r McpLocal) implementsConfigMcp() {}
+func (r McpLocalConfig) implementsConfigMcp() {}
 
 
 // Type of MCP server connection
 // Type of MCP server connection
-type McpLocalType string
+type McpLocalConfigType string
 
 
 const (
 const (
-	McpLocalTypeLocal McpLocalType = "local"
+	McpLocalConfigTypeLocal McpLocalConfigType = "local"
 )
 )
 
 
-func (r McpLocalType) IsKnown() bool {
+func (r McpLocalConfigType) IsKnown() bool {
 	switch r {
 	switch r {
-	case McpLocalTypeLocal:
+	case McpLocalConfigTypeLocal:
 		return true
 		return true
 	}
 	}
 	return false
 	return false
 }
 }
 
 
-type McpRemote struct {
+type McpRemoteConfig struct {
 	// Type of MCP server connection
 	// Type of MCP server connection
-	Type McpRemoteType `json:"type,required"`
+	Type McpRemoteConfigType `json:"type,required"`
 	// URL of the remote MCP server
 	// URL of the remote MCP server
 	URL string `json:"url,required"`
 	URL string `json:"url,required"`
 	// Enable or disable the MCP server on startup
 	// 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
 	Type        apijson.Field
 	URL         apijson.Field
 	URL         apijson.Field
 	Enabled     apijson.Field
 	Enabled     apijson.Field
@@ -684,166 +651,51 @@ type mcpRemoteJSON struct {
 	ExtraFields map[string]apijson.Field
 	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)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r mcpRemoteJSON) RawJSON() string {
+func (r mcpRemoteConfigJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }
 
 
-func (r McpRemote) implementsConfigMcp() {}
+func (r McpRemoteConfig) implementsConfigMcp() {}
 
 
 // Type of MCP server connection
 // Type of MCP server connection
-type McpRemoteType string
+type McpRemoteConfigType string
 
 
 const (
 const (
-	McpRemoteTypeRemote McpRemoteType = "remote"
+	McpRemoteConfigTypeRemote McpRemoteConfigType = "remote"
 )
 )
 
 
-func (r McpRemoteType) IsKnown() bool {
+func (r McpRemoteConfigType) IsKnown() bool {
 	switch r {
 	switch r {
-	case McpRemoteTypeRemote:
+	case McpRemoteConfigTypeRemote:
 		return true
 		return true
 	}
 	}
 	return false
 	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
 	raw         string
 	ExtraFields map[string]apijson.Field
 	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)
 	return apijson.UnmarshalRoot(data, r)
 }
 }
 
 
-func (r configProvidersResponseJSON) RawJSON() string {
+func (r modeConfigJSON) RawJSON() string {
 	return r.raw
 	return r.raw
 }
 }

+ 0 - 22
packages/tui/sdk/config_test.go

@@ -34,25 +34,3 @@ func TestConfigGet(t *testing.T) {
 		t.Fatalf("err should be nil: %s", err.Error())
 		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())
-	}
-}

+ 17 - 0
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`.
 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

+ 8 - 7
stainless.yml

@@ -49,12 +49,15 @@ resources:
     models:
     models:
       app: App
       app: App
       logLevel: LogLevel
       logLevel: LogLevel
+      provider: Provider
+      model: Model
       mode: Mode
       mode: Mode
     methods:
     methods:
       get: get /app
       get: get /app
       init: post /app/init
       init: post /app/init
       log: post /log
       log: post /log
       modes: get /mode
       modes: get /mode
+      providers: get /config/providers
 
 
   find:
   find:
     models:
     models:
@@ -75,15 +78,13 @@ resources:
   config:
   config:
     models:
     models:
       config: Config
       config: Config
-      keybinds: KeybindsConfig
-      mcpLocal: McpLocalConfig
-      mcpRemote: McpRemoteConfig
-      mode: ModeConfig
-      provider: Provider
-      model: Model
+      keybindsConfig: KeybindsConfig
+      mcpLocalConfig: McpLocalConfig
+      mcpRemoteConfig: McpRemoteConfig
+      modeConfig: ModeConfig
+      layoutConfig: LayoutConfig
     methods:
     methods:
       get: get /config
       get: get /config
-      providers: get /config/providers
 
 
   session:
   session:
     models:
     models: