summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-06-16 11:53:06 -0500
committeradamdottv <[email protected]>2025-06-16 11:54:55 -0500
commit7c0d10a4cec17d4cb2e04793c56363f2e746278b (patch)
treedd1fef7560a8bdb7803e40e1fa31ab51d7134bad
parent06af4061469b584744e4a976999bb7a55885c15d (diff)
downloadopencode-7c0d10a4cec17d4cb2e04793c56363f2e746278b.tar.gz
opencode-7c0d10a4cec17d4cb2e04793c56363f2e746278b.zip
feat: faster tui init
-rw-r--r--packages/opencode/src/index.ts2
-rw-r--r--packages/tui/cmd/opencode/main.go52
-rw-r--r--packages/tui/internal/app/app.go150
-rw-r--r--packages/tui/internal/components/chat/message.go4
-rw-r--r--packages/tui/internal/components/chat/messages.go4
-rw-r--r--packages/tui/internal/components/core/status.go8
-rw-r--r--packages/tui/internal/config/config.go10
-rw-r--r--packages/tui/internal/tui/tui.go4
8 files changed, 123 insertions, 111 deletions
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 45463d40c..c446b2033 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -47,7 +47,6 @@ const cli = yargs(hideBin(process.argv))
const result = await App.provide(
{ cwd, version: VERSION },
async (app) => {
- App.info().path.config
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
@@ -79,6 +78,7 @@ const cli = yargs(hideBin(process.argv))
env: {
...process.env,
OPENCODE_SERVER: server.url.toString(),
+ OPENCODE_APP_INFO: JSON.stringify(app),
},
onExit: () => {
server.stop()
diff --git a/packages/tui/cmd/opencode/main.go b/packages/tui/cmd/opencode/main.go
index 8595cb6f7..a46664806 100644
--- a/packages/tui/cmd/opencode/main.go
+++ b/packages/tui/cmd/opencode/main.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "encoding/json"
"log/slog"
"os"
"path/filepath"
@@ -16,7 +17,16 @@ import (
var Version = "dev"
func main() {
+ version := Version
+ if version != "dev" && !strings.HasPrefix(Version, "v") {
+ version = "v" + Version
+ }
+
url := os.Getenv("OPENCODE_SERVER")
+ appInfoStr := os.Getenv("OPENCODE_APP_INFO")
+ var appInfo client.AppInfo
+ json.Unmarshal([]byte(appInfoStr), &appInfo)
+
httpClient, err := client.NewClientWithResponses(url)
if err != nil {
slog.Error("Failed to create client", "error", err)
@@ -27,11 +37,7 @@ func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- version := Version
- if version != "dev" && !strings.HasPrefix(Version, "v") {
- version = "v" + Version
- }
- app_, err := app.New(ctx, version, httpClient)
+ app_, err := app.New(ctx, version, appInfo, httpClient)
if err != nil {
panic(err)
}
@@ -61,27 +67,29 @@ func main() {
}
}()
- paths, err := httpClient.PostPathGetWithResponse(context.Background())
- if err != nil {
- panic(err)
- }
- logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
+ go func() {
+ paths, err := httpClient.PostPathGetWithResponse(context.Background())
+ if err != nil {
+ panic(err)
+ }
+ logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
- if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
- err := os.MkdirAll(filepath.Dir(logfile), 0755)
+ if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
+ err := os.MkdirAll(filepath.Dir(logfile), 0755)
+ if err != nil {
+ slog.Error("Failed to create log directory", "error", err)
+ os.Exit(1)
+ }
+ }
+ file, err := os.Create(logfile)
if err != nil {
- slog.Error("Failed to create log directory", "error", err)
+ slog.Error("Failed to create log file", "error", err)
os.Exit(1)
}
- }
- file, err := os.Create(logfile)
- if err != nil {
- slog.Error("Failed to create log file", "error", err)
- os.Exit(1)
- }
- defer file.Close()
- logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
- slog.SetDefault(logger)
+ defer file.Close()
+ logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
+ slog.SetDefault(logger)
+ }()
// Run the TUI
result, err := program.Run()
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go
index 7602bfa17..658dbcbdf 100644
--- a/packages/tui/internal/app/app.go
+++ b/packages/tui/internal/app/app.go
@@ -17,7 +17,11 @@ import (
"github.com/sst/opencode/pkg/client"
)
+var RootPath string
+
type App struct {
+ Info client.AppInfo
+ Version string
ConfigPath string
Config *config.Config
Client *client.ClientWithResponses
@@ -28,95 +32,99 @@ type App struct {
Commands commands.Registry
}
-type AppInfo struct {
- client.AppInfo
- Version string
-}
-
-var Info AppInfo
-
-func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
- appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
- appInfo := appInfoResponse.JSON200
- Info = AppInfo{
- AppInfo: *appInfo,
- Version: version,
- }
-
- providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
- if err != nil {
- return nil, err
- }
- providers := []client.ProviderInfo{}
- var defaultProvider *client.ProviderInfo
- var defaultModel *client.ModelInfo
-
- var anthropic *client.ProviderInfo
- for _, provider := range providersResponse.JSON200.Providers {
- if provider.Id == "anthropic" {
- anthropic = &provider
- }
- }
-
- // default to anthropic if available
- if anthropic != nil {
- defaultProvider = anthropic
- defaultModel = getDefaultModel(providersResponse, *anthropic)
- }
+func New(
+ ctx context.Context,
+ version string,
+ appInfo client.AppInfo,
+ httpClient *client.ClientWithResponses,
+) (*App, error) {
+ RootPath = appInfo.Path.Root
- for _, provider := range providersResponse.JSON200.Providers {
- if defaultProvider == nil || defaultModel == nil {
- defaultProvider = &provider
- defaultModel = getDefaultModel(providersResponse, provider)
- }
- providers = append(providers, provider)
- }
- if len(providers) == 0 {
- return nil, fmt.Errorf("no providers found")
- }
-
- appConfigPath := filepath.Join(Info.Path.Config, "config")
+ appConfigPath := filepath.Join(appInfo.Path.Config, "config")
appConfig, err := config.LoadConfig(appConfigPath)
if err != nil {
- slog.Info("No TUI config found, using default values", "error", err)
- appConfig = config.NewConfig("opencode", defaultProvider.Id, defaultModel.Id)
+ appConfig = config.NewConfig()
config.SaveConfig(appConfigPath, appConfig)
}
-
- var currentProvider *client.ProviderInfo
- var currentModel *client.ModelInfo
- for _, provider := range providers {
- if provider.Id == appConfig.Provider {
- currentProvider = &provider
-
- for _, model := range provider.Models {
- if model.Id == appConfig.Model {
- currentModel = &model
- }
- }
- }
- }
- if currentProvider == nil || currentModel == nil {
- currentProvider = defaultProvider
- currentModel = defaultModel
- }
+ theme.SetTheme(appConfig.Theme)
app := &App{
+ Info: appInfo,
+ Version: version,
ConfigPath: appConfigPath,
Config: appConfig,
Client: httpClient,
- Provider: currentProvider,
- Model: currentModel,
Session: &client.SessionInfo{},
Messages: []client.MessageInfo{},
Commands: commands.NewCommandRegistry(),
}
- theme.SetTheme(appConfig.Theme)
-
return app, nil
}
+func (a *App) InitializeProvider() tea.Cmd {
+ return func() tea.Msg {
+ providersResponse, err := a.Client.PostProviderListWithResponse(context.Background())
+ if err != nil {
+ slog.Error("Failed to list providers", "error", err)
+ // TODO: notify user
+ return nil
+ }
+ providers := []client.ProviderInfo{}
+ var defaultProvider *client.ProviderInfo
+ var defaultModel *client.ModelInfo
+
+ var anthropic *client.ProviderInfo
+ for _, provider := range providersResponse.JSON200.Providers {
+ if provider.Id == "anthropic" {
+ anthropic = &provider
+ }
+ }
+
+ // default to anthropic if available
+ if anthropic != nil {
+ defaultProvider = anthropic
+ defaultModel = getDefaultModel(providersResponse, *anthropic)
+ }
+
+ for _, provider := range providersResponse.JSON200.Providers {
+ if defaultProvider == nil || defaultModel == nil {
+ defaultProvider = &provider
+ defaultModel = getDefaultModel(providersResponse, provider)
+ }
+ providers = append(providers, provider)
+ }
+ if len(providers) == 0 {
+ slog.Error("No providers configured")
+ return nil
+ }
+
+ var currentProvider *client.ProviderInfo
+ var currentModel *client.ModelInfo
+ for _, provider := range providers {
+ if provider.Id == a.Config.Provider {
+ currentProvider = &provider
+
+ for _, model := range provider.Models {
+ if model.Id == a.Config.Model {
+ currentModel = &model
+ }
+ }
+ }
+ }
+ if currentProvider == nil || currentModel == nil {
+ currentProvider = defaultProvider
+ currentModel = defaultModel
+ }
+
+ // TODO: handle no provider or model setup, yet
+ return state.ModelSelectedMsg{
+ Provider: *currentProvider,
+ Model: *currentModel,
+ }
+ }
+}
+
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
if match, ok := response.JSON200.Default[provider.Id]; ok {
model := provider.Models[match]
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 1f9f8ca59..17d643f5b 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -23,7 +23,7 @@ import (
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
r := styles.GetMarkdownRenderer(width, backgroundColor)
- content = strings.ReplaceAll(content, app.Info.Path.Root+"/", "")
+ content = strings.ReplaceAll(content, app.RootPath+"/", "")
rendered, _ := r.Render(content)
lines := strings.Split(rendered, "\n")
@@ -584,7 +584,7 @@ func truncateHeight(content string, height int) string {
}
func relative(path string) string {
- return strings.TrimPrefix(path, app.Info.Path.Root+"/")
+ return strings.TrimPrefix(path, app.RootPath+"/")
}
func extension(path string) string {
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 487e14a11..b59f96761 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -139,7 +139,7 @@ func (m *messagesComponent) renderView() {
author := ""
switch message.Role {
case client.User:
- author = app.Info.User
+ author = m.app.Info.User
case client.Assistant:
author = message.Metadata.Assistant.ModelID
}
@@ -328,7 +328,7 @@ func (m *messagesComponent) home() string {
logoAndVersion := lipgloss.JoinVertical(
lipgloss.Right,
logo,
- muted(app.Info.Version),
+ muted(m.app.Version),
)
lines := []string{}
diff --git a/packages/tui/internal/components/core/status.go b/packages/tui/internal/components/core/status.go
index d014d6fa3..e9b913642 100644
--- a/packages/tui/internal/components/core/status.go
+++ b/packages/tui/internal/components/core/status.go
@@ -34,14 +34,14 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
-func logo() string {
+func (m statusComponent) logo() string {
t := theme.CurrentTheme()
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
open := base("open")
code := emphasis("code ")
- version := base(app.Info.Version)
+ version := base(m.app.Version)
return styles.Padded().
Background(t.BackgroundElement()).
Render(open + code + version)
@@ -84,12 +84,12 @@ func (m statusComponent) View() string {
Render("")
}
- logo := logo()
+ logo := m.logo()
cwd := styles.Padded().
Foreground(t.TextMuted()).
Background(t.BackgroundSubtle()).
- Render(app.Info.Path.Cwd)
+ Render(m.app.Info.Path.Cwd)
sessionInfo := ""
if m.app.Session.Id != "" {
diff --git a/packages/tui/internal/config/config.go b/packages/tui/internal/config/config.go
index c9f32a424..ad7b18d07 100644
--- a/packages/tui/internal/config/config.go
+++ b/packages/tui/internal/config/config.go
@@ -17,11 +17,9 @@ type Config struct {
// NewConfig creates a new Config instance with default values.
// This can be useful for initializing a new configuration file.
-func NewConfig(theme, provider, model string) *Config {
+func NewConfig() *Config {
return &Config{
- Theme: theme,
- Provider: provider,
- Model: model,
+ Theme: "opencode",
}
}
@@ -35,12 +33,10 @@ func SaveConfig(filePath string, config *Config) error {
defer file.Close()
writer := bufio.NewWriter(file)
-
encoder := toml.NewEncoder(writer)
if err := encoder.Encode(config); err != nil {
return fmt.Errorf("failed to encode config to TOML file %s: %w", filePath, err)
}
-
if err := writer.Flush(); err != nil {
return fmt.Errorf("failed to flush writer for config file %s: %w", filePath, err)
}
@@ -53,13 +49,11 @@ func SaveConfig(filePath string, config *Config) error {
// It returns a pointer to the Config struct and an error if any issues occur.
func LoadConfig(filePath string) (*Config, error) {
var config Config
-
if _, err := toml.DecodeFile(filePath, &config); err != nil {
if _, statErr := os.Stat(filePath); os.IsNotExist(statErr) {
return nil, fmt.Errorf("config file not found at %s: %w", filePath, statErr)
}
return nil, fmt.Errorf("failed to decode TOML from file %s: %w", filePath, err)
}
-
return &config, nil
}
diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go
index 2cc203b6a..d9ce0180c 100644
--- a/packages/tui/internal/tui/tui.go
+++ b/packages/tui/internal/tui/tui.go
@@ -38,6 +38,8 @@ type appModel struct {
func (a appModel) Init() tea.Cmd {
t := theme.CurrentTheme()
var cmds []tea.Cmd
+ cmds = append(cmds, a.app.InitializeProvider())
+
cmds = append(cmds, tea.SetBackgroundColor(t.Background()))
cmds = append(cmds, tea.RequestBackgroundColor)
@@ -50,7 +52,7 @@ func (a appModel) Init() tea.Cmd {
// Check if we should show the init dialog
cmds = append(cmds, func() tea.Msg {
- shouldShow := app.Info.Git && app.Info.Time.Initialized == nil
+ shouldShow := a.app.Info.Git && a.app.Info.Time.Initialized == nil
return dialog.ShowInitDialogMsg{Show: shouldShow}
})