10 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/db"
14 "github.com/charmbracelet/crush/internal/format"
15 "github.com/charmbracelet/crush/internal/llm/agent"
16 "github.com/charmbracelet/crush/internal/logging"
17 "github.com/charmbracelet/crush/internal/pubsub"
18 "github.com/charmbracelet/crush/internal/tui"
19 "github.com/charmbracelet/crush/internal/version"
20 "github.com/charmbracelet/fang"
21 "github.com/spf13/cobra"
24var rootCmd = &cobra.Command{
26 Short: "Terminal-based AI assistant for software development",
27 Long: `Crush 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 crush -d -c /path/to/project
43 # Run a single non-interactive prompt
44 crush -p "Explain the use of context in Go"
46 # Run a single non-interactive prompt with JSON output format
47 crush -p "Explain the use of context in Go" -f json
49 RunE: func(cmd *cobra.Command, args []string) error {
51 debug, _ := cmd.Flags().GetBool("debug")
52 cwd, _ := cmd.Flags().GetString("cwd")
53 prompt, _ := cmd.Flags().GetString("prompt")
54 outputFormat, _ := cmd.Flags().GetString("output-format")
55 quiet, _ := cmd.Flags().GetBool("quiet")
57 // Validate format option
58 if !format.IsValid(outputFormat) {
59 return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
65 return fmt.Errorf("failed to change directory: %v", err)
71 return fmt.Errorf("failed to get current working directory: %v", err)
75 _, err := config.Load(cwd, debug)
80 // Connect DB, this will also run migrations
81 conn, err := db.Connect()
86 // Create main context for the application
87 ctx, cancel := context.WithCancel(context.Background())
90 app, err := app.New(ctx, conn)
92 logging.Error("Failed to create app: %v", err)
95 // Defer shutdown here so it runs for both interactive and non-interactive modes
98 // Initialize MCP tools early for both modes
99 initMCPTools(ctx, app)
101 // Non-interactive mode
103 // Run non-interactive flow using the App method
104 return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
108 program := tea.NewProgram(
111 tea.WithKeyReleases(),
112 tea.WithUniformKeyLayout(),
115 // Setup the subscriptions, this will send services events to the TUI
116 ch, cancelSubs := setupSubscriptions(app, ctx)
118 // Create a context for the TUI message handler
119 tuiCtx, tuiCancel := context.WithCancel(ctx)
120 var tuiWg sync.WaitGroup
123 // Set up message handling for the TUI
126 defer logging.RecoverPanic("TUI-message-handler", func() {
127 attemptTUIRecovery(program)
132 case <-tuiCtx.Done():
133 logging.Info("TUI message handler shutting down")
135 case msg, ok := <-ch:
137 logging.Info("TUI message channel closed")
145 // Cleanup function for when the program exits
150 // Cancel subscriptions first
153 // Then cancel TUI message handler
156 // Wait for TUI message handler to finish
159 logging.Info("All goroutines cleaned up")
163 result, err := program.Run()
167 logging.Error("TUI error: %v", err)
168 return fmt.Errorf("TUI error: %v", err)
171 logging.Info("TUI exited with result: %v", result)
176// attemptTUIRecovery tries to recover the TUI after a panic
177func attemptTUIRecovery(program *tea.Program) {
178 logging.Info("Attempting to recover TUI after panic")
180 // We could try to restart the TUI or gracefully exit
181 // For now, we'll just quit the program to avoid further issues
185func initMCPTools(ctx context.Context, app *app.App) {
187 defer logging.RecoverPanic("MCP-goroutine", nil)
189 // Create a context with timeout for the initial MCP tools fetch
190 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
193 // Set this up once with proper error handling
194 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
195 logging.Info("MCP message handling goroutine exiting")
199func setupSubscriber[T any](
203 subscriber func(context.Context) <-chan pubsub.Event[T],
204 outputCh chan<- tea.Msg,
209 defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
211 subCh := subscriber(ctx)
215 case event, ok := <-subCh:
217 logging.Info("subscription channel closed", "name", name)
221 var msg tea.Msg = event
224 case outputCh <- msg:
225 case <-time.After(2 * time.Second):
226 logging.Warn("message dropped due to slow consumer", "name", name)
228 logging.Info("subscription cancelled", "name", name)
232 logging.Info("subscription cancelled", "name", name)
239func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
240 ch := make(chan tea.Msg, 100)
242 wg := sync.WaitGroup{}
243 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
245 setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
246 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
247 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
248 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
249 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
250 setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
252 cleanupFunc := func() {
253 logging.Info("Cancelling all subscriptions")
254 cancel() // Signal all goroutines to stop
256 waitCh := make(chan struct{})
258 defer logging.RecoverPanic("subscription-cleanup", nil)
265 logging.Info("All subscription goroutines completed successfully")
266 close(ch) // Only close after all writers are confirmed done
267 case <-time.After(5 * time.Second):
268 logging.Warn("Timed out waiting for some subscription goroutines to complete")
272 return ch, cleanupFunc
276 if err := fang.Execute(
277 context.Background(),
279 fang.WithVersion(version.Version),
286 rootCmd.Flags().BoolP("help", "h", false, "Help")
287 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
288 rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
289 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
291 // Add format flag with validation logic
292 rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
293 "Output format for non-interactive mode (text, json)")
295 // Add quiet flag to hide spinner in non-interactive mode
296 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
298 // Register custom validation for the format flag
299 rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
300 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