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