10 tea "github.com/charmbracelet/bubbletea"
11 zone "github.com/lrstanley/bubblezone"
12 "github.com/opencode-ai/opencode/internal/app"
13 "github.com/opencode-ai/opencode/internal/config"
14 "github.com/opencode-ai/opencode/internal/db"
15 "github.com/opencode-ai/opencode/internal/format"
16 "github.com/opencode-ai/opencode/internal/llm/agent"
17 "github.com/opencode-ai/opencode/internal/logging"
18 "github.com/opencode-ai/opencode/internal/pubsub"
19 "github.com/opencode-ai/opencode/internal/tui"
20 "github.com/opencode-ai/opencode/internal/version"
21 "github.com/spf13/cobra"
24var rootCmd = &cobra.Command{
26 Short: "Terminal-based AI assistant for software development",
27 Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks.
28It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
29to assist developers in writing, debugging, and understanding code directly from the terminal.`,
31 # Run in interactive mode
34 # Run with debug logging
37 # Run with debug logging in a specific directory
38 opencode -d -c /path/to/project
43 # Run a single non-interactive prompt
44 opencode -p "Explain the use of context in Go"
46 # Run a single non-interactive prompt with JSON output format
47 opencode -p "Explain the use of context in Go" -f json
49 RunE: func(cmd *cobra.Command, args []string) error {
50 // If the help flag is set, show the help message
51 if cmd.Flag("help").Changed {
55 if cmd.Flag("version").Changed {
56 fmt.Println(version.Version)
61 debug, _ := cmd.Flags().GetBool("debug")
62 cwd, _ := cmd.Flags().GetString("cwd")
63 prompt, _ := cmd.Flags().GetString("prompt")
64 outputFormat, _ := cmd.Flags().GetString("output-format")
65 quiet, _ := cmd.Flags().GetBool("quiet")
67 // Validate format option
68 if !format.IsValid(outputFormat) {
69 return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
75 return fmt.Errorf("failed to change directory: %v", err)
81 return fmt.Errorf("failed to get current working directory: %v", err)
85 _, err := config.Load(cwd, debug)
90 // Connect DB, this will also run migrations
91 conn, err := db.Connect()
96 // Create main context for the application
97 ctx, cancel := context.WithCancel(context.Background())
100 app, err := app.New(ctx, conn)
102 logging.Error("Failed to create app: %v", err)
105 // Defer shutdown here so it runs for both interactive and non-interactive modes
108 // Initialize MCP tools early for both modes
109 initMCPTools(ctx, app)
111 // Non-interactive mode
113 // Run non-interactive flow using the App method
114 return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
120 program := tea.NewProgram(
125 // Setup the subscriptions, this will send services events to the TUI
126 ch, cancelSubs := setupSubscriptions(app, ctx)
128 // Create a context for the TUI message handler
129 tuiCtx, tuiCancel := context.WithCancel(ctx)
130 var tuiWg sync.WaitGroup
133 // Set up message handling for the TUI
136 defer logging.RecoverPanic("TUI-message-handler", func() {
137 attemptTUIRecovery(program)
142 case <-tuiCtx.Done():
143 logging.Info("TUI message handler shutting down")
145 case msg, ok := <-ch:
147 logging.Info("TUI message channel closed")
155 // Cleanup function for when the program exits
160 // Cancel subscriptions first
163 // Then cancel TUI message handler
166 // Wait for TUI message handler to finish
169 logging.Info("All goroutines cleaned up")
173 result, err := program.Run()
177 logging.Error("TUI error: %v", err)
178 return fmt.Errorf("TUI error: %v", err)
181 logging.Info("TUI exited with result: %v", result)
186// attemptTUIRecovery tries to recover the TUI after a panic
187func attemptTUIRecovery(program *tea.Program) {
188 logging.Info("Attempting to recover TUI after panic")
190 // We could try to restart the TUI or gracefully exit
191 // For now, we'll just quit the program to avoid further issues
195func initMCPTools(ctx context.Context, app *app.App) {
197 defer logging.RecoverPanic("MCP-goroutine", nil)
199 // Create a context with timeout for the initial MCP tools fetch
200 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
203 // Set this up once with proper error handling
204 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
205 logging.Info("MCP message handling goroutine exiting")
209func setupSubscriber[T any](
213 subscriber func(context.Context) <-chan pubsub.Event[T],
214 outputCh chan<- tea.Msg,
219 defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
221 subCh := subscriber(ctx)
225 case event, ok := <-subCh:
227 logging.Info("subscription channel closed", "name", name)
231 var msg tea.Msg = event
234 case outputCh <- msg:
235 case <-time.After(2 * time.Second):
236 logging.Warn("message dropped due to slow consumer", "name", name)
238 logging.Info("subscription cancelled", "name", name)
242 logging.Info("subscription cancelled", "name", name)
249func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
250 ch := make(chan tea.Msg, 100)
252 wg := sync.WaitGroup{}
253 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
255 setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
258 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
259 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
261 cleanupFunc := func() {
262 logging.Info("Cancelling all subscriptions")
263 cancel() // Signal all goroutines to stop
265 waitCh := make(chan struct{})
267 defer logging.RecoverPanic("subscription-cleanup", nil)
274 logging.Info("All subscription goroutines completed successfully")
275 close(ch) // Only close after all writers are confirmed done
276 case <-time.After(5 * time.Second):
277 logging.Warn("Timed out waiting for some subscription goroutines to complete")
281 return ch, cleanupFunc
285 err := rootCmd.Execute()
292 rootCmd.Flags().BoolP("help", "h", false, "Help")
293 rootCmd.Flags().BoolP("version", "v", false, "Version")
294 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
295 rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
296 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
298 // Add format flag with validation logic
299 rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
300 "Output format for non-interactive mode (text, json)")
302 // Add quiet flag to hide spinner in non-interactive mode
303 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
305 // Register custom validation for the format flag
306 rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
307 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