wip: refactoring tui
This commit is contained in:
@@ -3,14 +3,15 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"log/slog"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/fileutil"
|
||||
"github.com/sst/opencode/internal/status"
|
||||
"github.com/sst/opencode/internal/tui/config"
|
||||
"github.com/sst/opencode/internal/tui/state"
|
||||
"github.com/sst/opencode/internal/tui/theme"
|
||||
"github.com/sst/opencode/internal/tui/util"
|
||||
@@ -18,20 +19,15 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
Paths *struct {
|
||||
Config string `json:"config"`
|
||||
Cwd string `json:"cwd"`
|
||||
Data string `json:"data"`
|
||||
Root string `json:"root"`
|
||||
}
|
||||
Client *client.ClientWithResponses
|
||||
Provider *client.ProviderInfo
|
||||
Model *client.ProviderModel
|
||||
Session *client.SessionInfo
|
||||
Messages []client.MessageInfo
|
||||
Status status.Service
|
||||
|
||||
PrimaryAgentOLD AgentService
|
||||
ConfigPath string
|
||||
Config *config.Config
|
||||
Info *client.AppInfo
|
||||
Client *client.ClientWithResponses
|
||||
Provider *client.ProviderInfo
|
||||
Model *client.ProviderModel
|
||||
Session *client.SessionInfo
|
||||
Messages []client.MessageInfo
|
||||
Status status.Service
|
||||
|
||||
// UI state
|
||||
filepickerOpen bool
|
||||
@@ -45,23 +41,71 @@ func New(ctx context.Context, httpClient *client.ClientWithResponses) (*App, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileutil.Init()
|
||||
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
|
||||
appInfo := appInfoResponse.JSON200
|
||||
providersResponse, _ := httpClient.PostProviderListWithResponse(ctx)
|
||||
providers := []client.ProviderInfo{}
|
||||
var defaultProvider *client.ProviderInfo
|
||||
var defaultModel *client.ProviderModel
|
||||
|
||||
paths, _ := httpClient.PostPathGetWithResponse(context.Background())
|
||||
for _, provider := range *providersResponse.JSON200 {
|
||||
if provider.Id == "anthropic" {
|
||||
defaultProvider = &provider
|
||||
|
||||
agentBridge := NewAgentServiceBridge(httpClient)
|
||||
|
||||
app := &App{
|
||||
Paths: paths.JSON200,
|
||||
Client: httpClient,
|
||||
Session: &client.SessionInfo{},
|
||||
Messages: []client.MessageInfo{},
|
||||
PrimaryAgentOLD: agentBridge,
|
||||
Status: status.GetService(),
|
||||
for _, model := range provider.Models {
|
||||
if model.Id == "claude-sonnet-4-20250514" {
|
||||
defaultModel = &model
|
||||
}
|
||||
}
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
return nil, fmt.Errorf("no providers found")
|
||||
}
|
||||
if defaultProvider == nil {
|
||||
defaultProvider = &providers[0]
|
||||
}
|
||||
if defaultModel == nil {
|
||||
defaultModel = &defaultProvider.Models[0]
|
||||
}
|
||||
|
||||
// Initialize theme based on configuration
|
||||
app.initTheme()
|
||||
appConfigPath := filepath.Join(appInfo.Path.Config, "tui.toml")
|
||||
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)
|
||||
config.SaveConfig(appConfigPath, appConfig)
|
||||
}
|
||||
|
||||
var currentProvider *client.ProviderInfo
|
||||
var currentModel *client.ProviderModel
|
||||
for _, provider := range providers {
|
||||
if provider.Id == appConfig.Provider {
|
||||
currentProvider = &provider
|
||||
|
||||
for _, model := range provider.Models {
|
||||
if model.Id == appConfig.Model {
|
||||
currentModel = &model
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app := &App{
|
||||
ConfigPath: appConfigPath,
|
||||
Config: appConfig,
|
||||
Info: appInfo,
|
||||
Client: httpClient,
|
||||
Provider: currentProvider,
|
||||
Model: currentModel,
|
||||
Session: &client.SessionInfo{},
|
||||
Messages: []client.MessageInfo{},
|
||||
Status: status.GetService(),
|
||||
}
|
||||
|
||||
theme.SetTheme(appConfig.Theme)
|
||||
fileutil.Init()
|
||||
|
||||
return app, nil
|
||||
}
|
||||
@@ -73,6 +117,10 @@ type Attachment struct {
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func (a *App) SaveConfig() {
|
||||
config.SaveConfig(a.ConfigPath, a.Config)
|
||||
}
|
||||
|
||||
// Create creates a new session
|
||||
func (a *App) SendChatMessage(ctx context.Context, text string, attachments []Attachment) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
@@ -169,22 +217,6 @@ func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error)
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// initTheme sets the application theme based on the configuration
|
||||
func (app *App) initTheme() {
|
||||
cfg := config.Get()
|
||||
if cfg == nil || cfg.TUI.Theme == "" {
|
||||
return // Use default theme
|
||||
}
|
||||
|
||||
// Try to set the theme from config
|
||||
err := theme.SetTheme(cfg.TUI.Theme)
|
||||
if err != nil {
|
||||
slog.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
|
||||
} else {
|
||||
slog.Debug("Set theme from config", "theme", cfg.TUI.Theme)
|
||||
}
|
||||
}
|
||||
|
||||
// IsFilepickerOpen returns whether the filepicker is currently open
|
||||
func (app *App) IsFilepickerOpen() bool {
|
||||
return app.filepickerOpen
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
|
||||
// AgentServiceBridge provides a minimal agent service that sends messages to the API
|
||||
type AgentServiceBridge struct {
|
||||
client *client.ClientWithResponses
|
||||
}
|
||||
|
||||
// NewAgentServiceBridge creates a new agent service bridge
|
||||
func NewAgentServiceBridge(client *client.ClientWithResponses) *AgentServiceBridge {
|
||||
return &AgentServiceBridge{client: client}
|
||||
}
|
||||
|
||||
// Cancel cancels the current generation - NOT IMPLEMENTED IN API YET
|
||||
func (a *AgentServiceBridge) Cancel(sessionID string) error {
|
||||
// TODO: Not implemented in TypeScript API yet
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsBusy checks if the agent is busy - NOT IMPLEMENTED IN API YET
|
||||
func (a *AgentServiceBridge) IsBusy() bool {
|
||||
// TODO: Not implemented in TypeScript API yet
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSessionBusy checks if the agent is busy for a specific session - NOT IMPLEMENTED IN API YET
|
||||
func (a *AgentServiceBridge) IsSessionBusy(sessionID string) bool {
|
||||
// TODO: Not implemented in TypeScript API yet
|
||||
return false
|
||||
}
|
||||
|
||||
// CompactSession compacts a session - NOT IMPLEMENTED IN API YET
|
||||
func (a *AgentServiceBridge) CompactSession(ctx context.Context, sessionID string, force bool) error {
|
||||
// TODO: Not implemented in TypeScript API yet
|
||||
return fmt.Errorf("session compaction not implemented in API")
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// AgentService defines the interface for agent operations
|
||||
type AgentService interface {
|
||||
Cancel(sessionID string) error
|
||||
IsBusy() bool
|
||||
IsSessionBusy(sessionID string) bool
|
||||
CompactSession(ctx context.Context, sessionID string, force bool) error
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func repo(width int) string {
|
||||
}
|
||||
|
||||
func cwd(app *app.App, width int) string {
|
||||
cwd := fmt.Sprintf("cwd: %s", app.Paths.Cwd)
|
||||
cwd := fmt.Sprintf("cwd: %s", app.Info.Path.Cwd)
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
return styles.BaseStyle().
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/tui/components/diff"
|
||||
"github.com/sst/opencode/internal/tui/styles"
|
||||
"github.com/sst/opencode/internal/tui/theme"
|
||||
@@ -27,7 +26,7 @@ func toMarkdown(content string, width int) string {
|
||||
return strings.TrimSuffix(rendered, "\n")
|
||||
}
|
||||
|
||||
func renderUserMessage(msg client.MessageInfo, width int) string {
|
||||
func renderUserMessage(user string, msg client.MessageInfo, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
style := styles.BaseStyle().
|
||||
BorderLeft(true).
|
||||
@@ -55,10 +54,9 @@ func renderUserMessage(msg client.MessageInfo, width int) string {
|
||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
||||
timestamp = timestamp[12:]
|
||||
}
|
||||
username, _ := config.GetUsername()
|
||||
info := styles.Padded().
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf("%s (%s)", username, timestamp))
|
||||
Render(fmt.Sprintf("%s (%s)", user, timestamp))
|
||||
|
||||
content := ""
|
||||
// if len(styledAttachments) > 0 {
|
||||
@@ -312,23 +310,6 @@ func renderParams(paramsWidth int, params ...string) string {
|
||||
return ansi.Truncate(mainParam, paramsWidth, "...")
|
||||
}
|
||||
|
||||
func removeWorkingDirPrefix(path string) string {
|
||||
wd := config.WorkingDirectory()
|
||||
if strings.HasPrefix(path, wd) {
|
||||
path = strings.TrimPrefix(path, wd)
|
||||
}
|
||||
if strings.HasPrefix(path, "/") {
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
}
|
||||
if strings.HasPrefix(path, "./") {
|
||||
path = strings.TrimPrefix(path, "./")
|
||||
}
|
||||
if strings.HasPrefix(path, "../") {
|
||||
path = strings.TrimPrefix(path, "../")
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func renderToolParams(paramWidth int, toolCall any) string {
|
||||
params := ""
|
||||
switch toolCall {
|
||||
|
||||
@@ -105,7 +105,7 @@ func (m *messagesCmp) renderView() {
|
||||
for _, msg := range m.app.Messages {
|
||||
switch msg.Role {
|
||||
case client.User:
|
||||
content := renderUserMessage(msg, m.width)
|
||||
content := renderUserMessage(m.app.Info.User, msg, m.width)
|
||||
messages = append(messages, content+"\n")
|
||||
case client.Assistant:
|
||||
content := renderAssistantMessage(msg, m.width, m.showToolMessages)
|
||||
@@ -245,28 +245,28 @@ func (m *messagesCmp) help() string {
|
||||
|
||||
text := ""
|
||||
|
||||
if m.app.PrimaryAgentOLD.IsBusy() {
|
||||
text += lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"),
|
||||
)
|
||||
} else {
|
||||
text += lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" ↑↓"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"),
|
||||
)
|
||||
}
|
||||
// if m.app.PrimaryAgentOLD.IsBusy() {
|
||||
// text += lipgloss.JoinHorizontal(
|
||||
// lipgloss.Left,
|
||||
// baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
|
||||
// baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
|
||||
// baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"),
|
||||
// )
|
||||
// } else {
|
||||
text += lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" ↑↓"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"),
|
||||
)
|
||||
// }
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(text)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/tui/app"
|
||||
"github.com/sst/opencode/internal/tui/state"
|
||||
"github.com/sst/opencode/internal/tui/styles"
|
||||
@@ -211,10 +210,3 @@ func NewSidebarCmp(app *app.App) tea.Model {
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get the display path for a file
|
||||
func getDisplayPath(path string) string {
|
||||
workingDir := config.WorkingDirectory()
|
||||
displayPath := strings.TrimPrefix(path, workingDir)
|
||||
return strings.TrimPrefix(displayPath, "/")
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
type StatusCmp interface {
|
||||
tea.Model
|
||||
SetHelpWidgetMsg(string)
|
||||
}
|
||||
|
||||
type statusCmp struct {
|
||||
@@ -99,14 +98,10 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var helpWidget = ""
|
||||
|
||||
// getHelpWidget returns the help widget with current theme colors
|
||||
func getHelpWidget(helpText string) string {
|
||||
func getHelpWidget() string {
|
||||
t := theme.CurrentTheme()
|
||||
if helpText == "" {
|
||||
helpText = "ctrl+? help"
|
||||
}
|
||||
helpText := "ctrl+? help"
|
||||
|
||||
return styles.Padded().
|
||||
Background(t.TextMuted()).
|
||||
@@ -145,8 +140,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
||||
|
||||
func (m statusCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
// Initialize the help widget
|
||||
status := getHelpWidget("")
|
||||
status := getHelpWidget()
|
||||
|
||||
if m.app.Session.Id != "" {
|
||||
tokens := float32(0)
|
||||
@@ -343,15 +337,7 @@ func (m statusCmp) model() string {
|
||||
Render(model)
|
||||
}
|
||||
|
||||
func (m statusCmp) SetHelpWidgetMsg(s string) {
|
||||
// Update the help widget text using the getHelpWidget function
|
||||
helpWidget = getHelpWidget(s)
|
||||
}
|
||||
|
||||
func NewStatusCmp(app *app.App) StatusCmp {
|
||||
// Initialize the help widget with default text
|
||||
helpWidget = getHelpWidget("")
|
||||
|
||||
statusComponent := &statusCmp{
|
||||
app: app,
|
||||
queue: []status.StatusMessage{},
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/tui/app"
|
||||
"github.com/sst/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
@@ -22,50 +22,19 @@ const (
|
||||
var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
|
||||
|
||||
// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
|
||||
func LoadCustomCommands() ([]Command, error) {
|
||||
cfg := config.Get()
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config not loaded")
|
||||
}
|
||||
|
||||
func LoadCustomCommands(app *app.App) ([]Command, error) {
|
||||
var commands []Command
|
||||
|
||||
// Load user commands from XDG_CONFIG_HOME/opencode/commands
|
||||
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgConfigHome == "" {
|
||||
// Default to ~/.config if XDG_CONFIG_HOME is not set
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
xdgConfigHome = filepath.Join(home, ".config")
|
||||
}
|
||||
homeCommandsDir := filepath.Join(app.Info.Path.Config, "commands")
|
||||
homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but continue - we'll still try to load other commands
|
||||
fmt.Printf("Warning: failed to load home commands: %v\n", err)
|
||||
} else {
|
||||
commands = append(commands, homeCommands...)
|
||||
}
|
||||
|
||||
if xdgConfigHome != "" {
|
||||
userCommandsDir := filepath.Join(xdgConfigHome, "opencode", "commands")
|
||||
userCommands, err := loadCommandsFromDir(userCommandsDir, UserCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but continue - we'll still try to load other commands
|
||||
fmt.Printf("Warning: failed to load user commands from XDG_CONFIG_HOME: %v\n", err)
|
||||
} else {
|
||||
commands = append(commands, userCommands...)
|
||||
}
|
||||
}
|
||||
|
||||
// Load commands from $HOME/.opencode/commands
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
homeCommandsDir := filepath.Join(home, ".opencode", "commands")
|
||||
homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but continue - we'll still try to load other commands
|
||||
fmt.Printf("Warning: failed to load home commands: %v\n", err)
|
||||
} else {
|
||||
commands = append(commands, homeCommands...)
|
||||
}
|
||||
}
|
||||
|
||||
// Load project commands from data directory
|
||||
projectCommandsDir := filepath.Join(cfg.Data.Directory, "commands")
|
||||
projectCommandsDir := filepath.Join(app.Info.Path.Root, "commands")
|
||||
projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but return what we have so far
|
||||
|
||||
@@ -110,7 +110,7 @@ func (m InitDialogCmp) View() string {
|
||||
Foreground(t.Text()).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Initialization generates a new CONTEXT.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
|
||||
Render("Initialization generates a new AGENTS.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
|
||||
|
||||
question := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
|
||||
65
packages/tui/internal/tui/config/config.go
Normal file
65
packages/tui/internal/tui/config/config.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Theme string `toml:"Theme"`
|
||||
Provider string `toml:"Provider"`
|
||||
Model string `toml:"Model"`
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return &Config{
|
||||
Theme: theme,
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
}
|
||||
}
|
||||
|
||||
// SaveConfig writes the provided Config struct to the specified TOML file.
|
||||
// It will create the file if it doesn't exist, or overwrite it if it does.
|
||||
func SaveConfig(filePath string, config *Config) error {
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create/open config file %s: %w", filePath, err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
slog.Debug("Configuration saved to file", "file", filePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfig reads a Config struct from the specified TOML file.
|
||||
// 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
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/sst/opencode/internal/completions"
|
||||
"github.com/sst/opencode/internal/status"
|
||||
"github.com/sst/opencode/internal/tui/app"
|
||||
"github.com/sst/opencode/internal/tui/components/chat"
|
||||
"github.com/sst/opencode/internal/tui/components/dialog"
|
||||
@@ -76,10 +75,10 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case dialog.CommandRunCustomMsg:
|
||||
// Check if the agent is busy before executing custom commands
|
||||
if p.app.PrimaryAgentOLD.IsBusy() {
|
||||
status.Warn("Agent is busy, please wait before executing a command...")
|
||||
return p, nil
|
||||
}
|
||||
// if p.app.PrimaryAgentOLD.IsBusy() {
|
||||
// status.Warn("Agent is busy, please wait before executing a command...")
|
||||
// return p, nil
|
||||
// }
|
||||
|
||||
// Process the command content with arguments if any
|
||||
content := msg.Content
|
||||
|
||||
@@ -17,3 +17,5 @@ type CompactSessionMsg struct{}
|
||||
type StateUpdatedMsg struct {
|
||||
State map[string]any
|
||||
}
|
||||
|
||||
// TODO: store in CONFIG/tui.yaml
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
)
|
||||
|
||||
// Manager handles theme registration, selection, and retrieval.
|
||||
@@ -47,35 +46,29 @@ func RegisterTheme(name string, theme Theme) {
|
||||
func SetTheme(name string) error {
|
||||
globalManager.mu.Lock()
|
||||
defer globalManager.mu.Unlock()
|
||||
|
||||
delete(styles.Registry, "charm")
|
||||
|
||||
// Handle custom theme
|
||||
if name == "custom" {
|
||||
cfg := config.Get()
|
||||
if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
|
||||
return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
|
||||
}
|
||||
// if name == "custom" {
|
||||
// cfg := config.Get()
|
||||
// if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
|
||||
// return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
|
||||
// }
|
||||
//
|
||||
// customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to load custom theme: %w", err)
|
||||
// }
|
||||
//
|
||||
// // Register the custom theme
|
||||
// globalManager.themes["custom"] = customTheme
|
||||
|
||||
customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load custom theme: %w", err)
|
||||
}
|
||||
|
||||
// Register the custom theme
|
||||
globalManager.themes["custom"] = customTheme
|
||||
} else if _, exists := globalManager.themes[name]; !exists {
|
||||
if _, exists := globalManager.themes[name]; !exists {
|
||||
return fmt.Errorf("theme '%s' not found", name)
|
||||
}
|
||||
|
||||
globalManager.currentName = name
|
||||
|
||||
// Update the config file using viper
|
||||
if err := updateConfigTheme(name); err != nil {
|
||||
// Log the error but don't fail the theme change
|
||||
slog.Warn("Warning: Failed to update config file with new theme", "err", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -257,9 +250,3 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
|
||||
|
||||
return theme, nil
|
||||
}
|
||||
|
||||
// updateConfigTheme updates the theme setting in the configuration file
|
||||
func updateConfigTheme(themeName string) error {
|
||||
// Use the config package to update the theme
|
||||
return config.UpdateTheme(themeName)
|
||||
}
|
||||
|
||||
@@ -10,11 +10,9 @@ import (
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/tui/app"
|
||||
|
||||
"github.com/sst/opencode/internal/status"
|
||||
"github.com/sst/opencode/internal/tui/components/chat"
|
||||
"github.com/sst/opencode/internal/tui/components/core"
|
||||
"github.com/sst/opencode/internal/tui/components/dialog"
|
||||
"github.com/sst/opencode/internal/tui/layout"
|
||||
@@ -160,20 +158,10 @@ func (a appModel) Init() tea.Cmd {
|
||||
|
||||
// Check if we should show the init dialog
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
shouldShow, err := config.ShouldShowInitDialog()
|
||||
if err != nil {
|
||||
status.Error("Failed to check init status: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
shouldShow := a.app.Info.Time.Initialized == nil
|
||||
return dialog.ShowInitDialogMsg{Show: shouldShow}
|
||||
})
|
||||
|
||||
// TODO: store last selected model somewhere
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
providers, _ := a.app.ListProviders(context.Background())
|
||||
return state.ModelSelectedMsg{Provider: providers[0], Model: providers[0].Models[0]}
|
||||
})
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -308,6 +296,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case state.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
a.app.Model = &msg.Model
|
||||
a.app.Config.Provider = msg.Provider.Id
|
||||
a.app.Config.Model = msg.Model.Id
|
||||
a.app.SaveConfig()
|
||||
return a.updateAllPages(msg)
|
||||
|
||||
case dialog.CloseCommandDialogMsg:
|
||||
@@ -327,6 +318,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, nil
|
||||
|
||||
case dialog.ThemeChangedMsg:
|
||||
a.app.Config.Theme = msg.ThemeName
|
||||
a.app.SaveConfig()
|
||||
|
||||
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
||||
a.showThemeDialog = false
|
||||
status.Info("Theme changed to: " + msg.ThemeName)
|
||||
@@ -342,21 +336,18 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// Run the initialization command
|
||||
for _, cmd := range a.commands {
|
||||
if cmd.ID == "init" {
|
||||
// Mark the project as initialized
|
||||
if err := config.MarkProjectInitialized(); err != nil {
|
||||
status.Error(err.Error())
|
||||
return a, nil
|
||||
}
|
||||
return a, cmd.Handler(cmd)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Mark the project as initialized without running the command
|
||||
if err := config.MarkProjectInitialized(); err != nil {
|
||||
status.Error(err.Error())
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
// TODO: should we not ask again?
|
||||
// else {
|
||||
// // Mark the project as initialized without running the command
|
||||
// if err := config.MarkProjectInitialized(); err != nil {
|
||||
// status.Error(err.Error())
|
||||
// return a, nil
|
||||
// }
|
||||
// }
|
||||
return a, nil
|
||||
|
||||
case dialog.CommandSelectedMsg:
|
||||
@@ -540,11 +531,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
if a.showInitDialog {
|
||||
a.showInitDialog = false
|
||||
// TODO: should we not ask again?
|
||||
// Mark the project as initialized without running the command
|
||||
if err := config.MarkProjectInitialized(); err != nil {
|
||||
status.Error(err.Error())
|
||||
return a, nil
|
||||
}
|
||||
// if err := config.MarkProjectInitialized(); err != nil {
|
||||
// status.Error(err.Error())
|
||||
// return a, nil
|
||||
// }
|
||||
return a, nil
|
||||
}
|
||||
if a.showFilepicker {
|
||||
@@ -566,13 +558,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, helpEsc):
|
||||
if a.app.PrimaryAgentOLD.IsBusy() {
|
||||
if a.showQuit {
|
||||
return a, nil
|
||||
}
|
||||
a.showHelp = !a.showHelp
|
||||
// if a.app.PrimaryAgentOLD.IsBusy() {
|
||||
if a.showQuit {
|
||||
return a, nil
|
||||
}
|
||||
a.showHelp = !a.showHelp
|
||||
return a, nil
|
||||
// }
|
||||
case key.Matches(msg, keys.Filepicker):
|
||||
// Toggle filepicker
|
||||
a.showFilepicker = !a.showFilepicker
|
||||
@@ -762,12 +754,6 @@ func (a appModel) View() string {
|
||||
|
||||
}
|
||||
|
||||
if !a.app.PrimaryAgentOLD.IsBusy() {
|
||||
a.status.SetHelpWidgetMsg("ctrl+? help")
|
||||
} else {
|
||||
a.status.SetHelpWidgetMsg("? help")
|
||||
}
|
||||
|
||||
if a.showHelp {
|
||||
bindings := layout.KeyMapToSlice(keys)
|
||||
if p, ok := a.pages[a.currentPage].(layout.Bindings); ok {
|
||||
@@ -776,9 +762,9 @@ func (a appModel) View() string {
|
||||
if a.showPermissions {
|
||||
bindings = append(bindings, a.permissions.BindingKeys()...)
|
||||
}
|
||||
if !a.app.PrimaryAgentOLD.IsBusy() {
|
||||
bindings = append(bindings, helpEsc)
|
||||
}
|
||||
// if !a.app.PrimaryAgentOLD.IsBusy() {
|
||||
bindings = append(bindings, helpEsc)
|
||||
// }
|
||||
a.help.SetBindings(bindings)
|
||||
|
||||
overlay := a.help.View()
|
||||
@@ -940,20 +926,12 @@ func New(app *app.App) tea.Model {
|
||||
model.RegisterCommand(dialog.Command{
|
||||
ID: "init",
|
||||
Title: "Initialize Project",
|
||||
Description: "Create/Update the CONTEXT.md memory file",
|
||||
Description: "Create/Update the AGENTS.md memory file",
|
||||
Handler: func(cmd dialog.Command) tea.Cmd {
|
||||
prompt := `Please analyze this codebase and create a CONTEXT.md file containing:
|
||||
1. Build/lint/test commands - especially for running a single test
|
||||
2. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
|
||||
|
||||
The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
|
||||
If there's already a CONTEXT.md, improve it.
|
||||
If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
|
||||
return tea.Batch(
|
||||
util.CmdHandler(chat.SendMsg{
|
||||
Text: prompt,
|
||||
}),
|
||||
)
|
||||
model.app.Client.PostSessionInitialize(context.Background(), client.PostSessionInitializeJSONRequestBody{
|
||||
SessionID: model.app.Session.Id,
|
||||
})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
@@ -975,7 +953,7 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
|
||||
})
|
||||
|
||||
// Load custom commands
|
||||
customCommands, err := dialog.LoadCustomCommands()
|
||||
customCommands, err := dialog.LoadCustomCommands(app)
|
||||
if err != nil {
|
||||
slog.Warn("Failed to load custom commands", "error", err)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user