wip: refactoring tui

This commit is contained in:
adamdottv
2025-06-03 15:47:47 -05:00
parent 1e063e7937
commit 0c6bda8255
22 changed files with 256 additions and 631 deletions

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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().

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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, "/")
}

View File

@@ -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{},

View File

@@ -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

View File

@@ -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()).

View 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
}

View File

@@ -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

View File

@@ -17,3 +17,5 @@ type CompactSessionMsg struct{}
type StateUpdatedMsg struct {
State map[string]any
}
// TODO: store in CONFIG/tui.yaml

View File

@@ -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)
}

View File

@@ -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 {