10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/opencode-ai/opencode/internal/app"
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/llm/agent"
16 "github.com/opencode-ai/opencode/internal/logging"
17 "github.com/opencode-ai/opencode/internal/pubsub"
18 "github.com/opencode-ai/opencode/internal/tui"
19 "github.com/opencode-ai/opencode/internal/version"
20 "github.com/spf13/cobra"
23var rootCmd = &cobra.Command{
25 Short: "Terminal-based AI assistant for software development",
26 Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
27It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
28to assist developers in writing, debugging, and understanding code directly from the terminal.`,
30 # Run in interactive mode
33 # Run with debug logging
36 # Run with debug logging in a specific directory
37 opencode -d -c /path/to/project
42 # Run a single non-interactive prompt
43 opencode -p "Explain the use of context in Go"
45 # Run a single non-interactive prompt with JSON output format
46 opencode -p "Explain the use of context in Go" -f json
48 RunE: func(cmd *cobra.Command, args []string) error {
49 // If the help flag is set, show the help message
50 if cmd.Flag("help").Changed {
54 if cmd.Flag("version").Changed {
55 fmt.Println(version.Version)
60 debug, _ := cmd.Flags().GetBool("debug")
61 cwd, _ := cmd.Flags().GetString("cwd")
62 prompt, _ := cmd.Flags().GetString("prompt")
63 outputFormat, _ := cmd.Flags().GetString("output-format")
64 quiet, _ := cmd.Flags().GetBool("quiet")
66 // Validate format option
67 if !format.IsValid(outputFormat) {
68 return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
74 return fmt.Errorf("failed to change directory: %v", err)
80 return fmt.Errorf("failed to get current working directory: %v", err)
84 _, err := config.Load(cwd, debug)
89 // Connect DB, this will also run migrations
90 conn, err := db.Connect()
95 // Create main context for the application
96 ctx, cancel := context.WithCancel(context.Background())
99 app, err := app.New(ctx, conn)
101 logging.Error("Failed to create app: %v", err)
104 // Defer shutdown here so it runs for both interactive and non-interactive modes
107 // Initialize MCP tools early for both modes
108 initMCPTools(ctx, app)
110 // Non-interactive mode
112 // Run non-interactive flow using the App method
113 return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
117 program := tea.NewProgram(
122 // Setup the subscriptions, this will send services events to the TUI
123 ch, cancelSubs := setupSubscriptions(app, ctx)
125 // Create a context for the TUI message handler
126 tuiCtx, tuiCancel := context.WithCancel(ctx)
127 var tuiWg sync.WaitGroup
130 // Set up message handling for the TUI
133 defer logging.RecoverPanic("TUI-message-handler", func() {
134 attemptTUIRecovery(program)
139 case <-tuiCtx.Done():
140 logging.Info("TUI message handler shutting down")
142 case msg, ok := <-ch:
144 logging.Info("TUI message channel closed")
152 // Cleanup function for when the program exits
157 // Cancel subscriptions first
160 // Then cancel TUI message handler
163 // Wait for TUI message handler to finish
166 logging.Info("All goroutines cleaned up")
170 result, err := program.Run()
174 logging.Error("TUI error: %v", err)
175 return fmt.Errorf("TUI error: %v", err)
178 logging.Info("TUI exited with result: %v", result)
183// attemptTUIRecovery tries to recover the TUI after a panic
184func attemptTUIRecovery(program *tea.Program) {
185 logging.Info("Attempting to recover TUI after panic")
187 // We could try to restart the TUI or gracefully exit
188 // For now, we'll just quit the program to avoid further issues
192func initMCPTools(ctx context.Context, app *app.App) {
194 defer logging.RecoverPanic("MCP-goroutine", nil)
196 // Create a context with timeout for the initial MCP tools fetch
197 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
200 // Set this up once with proper error handling
201 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
202 logging.Info("MCP message handling goroutine exiting")
206func setupSubscriber[T any](
210 subscriber func(context.Context) <-chan pubsub.Event[T],
211 outputCh chan<- tea.Msg,
216 defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
218 subCh := subscriber(ctx)
222 case event, ok := <-subCh:
224 logging.Info("subscription channel closed", "name", name)
228 var msg tea.Msg = event
231 case outputCh <- msg:
232 case <-time.After(2 * time.Second):
233 logging.Warn("message dropped due to slow consumer", "name", name)
235 logging.Info("subscription cancelled", "name", name)
239 logging.Info("subscription cancelled", "name", name)
246func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
247 ch := make(chan tea.Msg, 100)
249 wg := sync.WaitGroup{}
250 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
252 setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
253 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
254 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
255 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
258 cleanupFunc := func() {
259 logging.Info("Cancelling all subscriptions")
260 cancel() // Signal all goroutines to stop
262 waitCh := make(chan struct{})
264 defer logging.RecoverPanic("subscription-cleanup", nil)
271 logging.Info("All subscription goroutines completed successfully")
272 close(ch) // Only close after all writers are confirmed done
273 case <-time.After(5 * time.Second):
274 logging.Warn("Timed out waiting for some subscription goroutines to complete")
278 return ch, cleanupFunc
282 err := rootCmd.Execute()
289 rootCmd.Flags().BoolP("help", "h", false, "Help")
290 rootCmd.Flags().BoolP("version", "v", false, "Version")
291 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
292 rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
293 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
295 // Add format flag with validation logic
296 rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
297 "Output format for non-interactive mode (text, json)")
299 // Add quiet flag to hide spinner in non-interactive mode
300 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
302 // Register custom validation for the format flag
303 rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
304 return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list