1package app
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "maps"
9 "sync"
10 "time"
11
12 "github.com/opencode-ai/opencode/internal/config"
13 "github.com/opencode-ai/opencode/internal/db"
14 "github.com/opencode-ai/opencode/internal/format"
15 "github.com/opencode-ai/opencode/internal/history"
16 "github.com/opencode-ai/opencode/internal/llm/agent"
17 "github.com/opencode-ai/opencode/internal/logging"
18 "github.com/opencode-ai/opencode/internal/lsp"
19 "github.com/opencode-ai/opencode/internal/message"
20 "github.com/opencode-ai/opencode/internal/permission"
21 "github.com/opencode-ai/opencode/internal/session"
22 "github.com/opencode-ai/opencode/internal/tui/theme"
23)
24
25type App struct {
26 Sessions session.Service
27 Messages message.Service
28 History history.Service
29 Permissions permission.Service
30
31 CoderAgent agent.Service
32
33 LSPClients map[string]*lsp.Client
34
35 clientsMutex sync.RWMutex
36
37 watcherCancelFuncs []context.CancelFunc
38 cancelFuncsMutex sync.Mutex
39 watcherWG sync.WaitGroup
40}
41
42func New(ctx context.Context, conn *sql.DB) (*App, error) {
43 q := db.New(conn)
44 sessions := session.NewService(q)
45 messages := message.NewService(q)
46 files := history.NewService(q, conn)
47
48 app := &App{
49 Sessions: sessions,
50 Messages: messages,
51 History: files,
52 Permissions: permission.NewPermissionService(),
53 LSPClients: make(map[string]*lsp.Client),
54 }
55
56 // Initialize theme based on configuration
57 app.initTheme()
58
59 // Initialize LSP clients in the background
60 go app.initLSPClients(ctx)
61
62 var err error
63 app.CoderAgent, err = agent.NewAgent(
64 config.AgentCoder,
65 app.Sessions,
66 app.Messages,
67 agent.CoderAgentTools(
68 app.Permissions,
69 app.Sessions,
70 app.Messages,
71 app.History,
72 app.LSPClients,
73 ),
74 )
75 if err != nil {
76 logging.Error("Failed to create coder agent", err)
77 return nil, err
78 }
79
80 return app, nil
81}
82
83// initTheme sets the application theme based on the configuration
84func (app *App) initTheme() {
85 cfg := config.Get()
86 if cfg == nil || cfg.TUI.Theme == "" {
87 return // Use default theme
88 }
89
90 // Try to set the theme from config
91 err := theme.SetTheme(cfg.TUI.Theme)
92 if err != nil {
93 logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
94 } else {
95 logging.Debug("Set theme from config", "theme", cfg.TUI.Theme)
96 }
97}
98
99// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
100func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
101 logging.Info("Running in non-interactive mode")
102
103 // Start spinner if not in quiet mode
104 var spinner *format.Spinner
105 if !quiet {
106 spinner = format.NewSpinner("Thinking...")
107 spinner.Start()
108 defer spinner.Stop()
109 }
110
111 const maxPromptLengthForTitle = 100
112 titlePrefix := "Non-interactive: "
113 var titleSuffix string
114
115 if len(prompt) > maxPromptLengthForTitle {
116 titleSuffix = prompt[:maxPromptLengthForTitle] + "..."
117 } else {
118 titleSuffix = prompt
119 }
120 title := titlePrefix + titleSuffix
121
122 sess, err := a.Sessions.Create(ctx, title)
123 if err != nil {
124 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
125 }
126 logging.Info("Created session for non-interactive run", "session_id", sess.ID)
127
128 // Automatically approve all permission requests for this non-interactive session
129 a.Permissions.AutoApproveSession(sess.ID)
130
131 done, err := a.CoderAgent.Run(ctx, sess.ID, prompt)
132 if err != nil {
133 return fmt.Errorf("failed to start agent processing stream: %w", err)
134 }
135
136 result := <-done
137 if result.Error != nil {
138 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
139 logging.Info("Agent processing cancelled", "session_id", sess.ID)
140 return nil
141 }
142 return fmt.Errorf("agent processing failed: %w", result.Error)
143 }
144
145 // Stop spinner before printing output
146 if !quiet && spinner != nil {
147 spinner.Stop()
148 }
149
150 // Get the text content from the response
151 content := "No content available"
152 if result.Message.Content().String() != "" {
153 content = result.Message.Content().String()
154 }
155
156 fmt.Println(format.FormatOutput(content, outputFormat))
157
158 logging.Info("Non-interactive run completed", "session_id", sess.ID)
159
160 return nil
161}
162
163// Shutdown performs a clean shutdown of the application
164func (app *App) Shutdown() {
165 // Cancel all watcher goroutines
166 app.cancelFuncsMutex.Lock()
167 for _, cancel := range app.watcherCancelFuncs {
168 cancel()
169 }
170 app.cancelFuncsMutex.Unlock()
171 app.watcherWG.Wait()
172
173 // Perform additional cleanup for LSP clients
174 app.clientsMutex.RLock()
175 clients := make(map[string]*lsp.Client, len(app.LSPClients))
176 maps.Copy(clients, app.LSPClients)
177 app.clientsMutex.RUnlock()
178
179 for name, client := range clients {
180 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
181 if err := client.Shutdown(shutdownCtx); err != nil {
182 logging.Error("Failed to shutdown LSP client", "name", name, "error", err)
183 }
184 cancel()
185 }
186}