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)
76 _, err := config.Init(cwd, debug)
81 // Create main context for the application
82 ctx, cancel := context.WithCancel(context.Background())
85 // Connect DB, this will also run migrations
86 conn, err := db.Connect(ctx)
91 app, err := app.New(ctx, conn)
93 logging.Error("Failed to create app: %v", err)
96 // Defer shutdown here so it runs for both interactive and non-interactive modes
99 // Initialize MCP tools early for both modes
100 initMCPTools(ctx, app)
102 // Non-interactive mode
104 // Run non-interactive flow using the App method
105 return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
109 program := tea.NewProgram(
112 tea.WithKeyReleases(),
113 tea.WithUniformKeyLayout(),
116 // Setup the subscriptions, this will send services events to the TUI
117 ch, cancelSubs := setupSubscriptions(app, ctx)
119 // Create a context for the TUI message handler
120 tuiCtx, tuiCancel := context.WithCancel(ctx)
121 var tuiWg sync.WaitGroup
124 // Set up message handling for the TUI
127 defer logging.RecoverPanic("TUI-message-handler", func() {
128 attemptTUIRecovery(program)
133 case <-tuiCtx.Done():
134 logging.Info("TUI message handler shutting down")
136 case msg, ok := <-ch:
138 logging.Info("TUI message channel closed")
146 // Cleanup function for when the program exits
151 // Cancel subscriptions first
154 // Then cancel TUI message handler
157 // Wait for TUI message handler to finish
160 logging.Info("All goroutines cleaned up")
164 result, err := program.Run()
168 logging.Error("TUI error: %v", err)
169 return fmt.Errorf("TUI error: %v", err)
172 logging.Info("TUI exited with result: %v", result)
177// attemptTUIRecovery tries to recover the TUI after a panic
178func attemptTUIRecovery(program *tea.Program) {
179 logging.Info("Attempting to recover TUI after panic")
181 // We could try to restart the TUI or gracefully exit
182 // For now, we'll just quit the program to avoid further issues
186func initMCPTools(ctx context.Context, app *app.App) {
188 defer logging.RecoverPanic("MCP-goroutine", nil)
190 // Create a context with timeout for the initial MCP tools fetch
191 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
194 // Set this up once with proper error handling
195 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
196 logging.Info("MCP message handling goroutine exiting")
200func setupSubscriber[T any](
204 subscriber func(context.Context) <-chan pubsub.Event[T],
205 outputCh chan<- tea.Msg,
210 defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
212 subCh := subscriber(ctx)
216 case event, ok := <-subCh:
218 logging.Info("subscription channel closed", "name", name)
222 var msg tea.Msg = event
225 case outputCh <- msg:
226 case <-time.After(2 * time.Second):
227 logging.Warn("message dropped due to slow consumer", "name", name)
229 logging.Info("subscription cancelled", "name", name)
233 logging.Info("subscription cancelled", "name", name)
240func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
241 ch := make(chan tea.Msg, 100)
243 wg := sync.WaitGroup{}
244 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
246 setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
247 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
248 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
249 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
250 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
251 setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
253 cleanupFunc := func() {
254 logging.Info("Cancelling all subscriptions")
255 cancel() // Signal all goroutines to stop
257 waitCh := make(chan struct{})
259 defer logging.RecoverPanic("subscription-cleanup", nil)
266 logging.Info("All subscription goroutines completed successfully")
267 close(ch) // Only close after all writers are confirmed done
268 case <-time.After(5 * time.Second):
269 logging.Warn("Timed out waiting for some subscription goroutines to complete")
273 return ch, cleanupFunc
277 if err := fang.Execute(
278 context.Background(),
280 fang.WithVersion(version.Version),
287 rootCmd.Flags().BoolP("help", "h", false, "Help")
288 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
289 rootCmd.Flags().StringP("cwd", "c", "", "Current working directory")
290 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
292 // Add format flag with validation logic
293 rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
294 "Output format for non-interactive mode (text, json)")
296 // Add quiet flag to hide spinner in non-interactive mode
297 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
299 // Register custom validation for the format flag
300 rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
301 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