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/spf13/cobra"
23var rootCmd = &cobra.Command{
25 Short: "Terminal-based AI assistant for software development",
26 Long: `Crush 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 crush -d -c /path/to/project
42 # Run a single non-interactive prompt
43 crush -p "Explain the use of context in Go"
45 # Run a single non-interactive prompt with JSON output format
46 crush -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(
120 tea.WithKeyReleases(),
121 tea.WithUniformKeyLayout(),
124 // Setup the subscriptions, this will send services events to the TUI
125 ch, cancelSubs := setupSubscriptions(app, ctx)
127 // Create a context for the TUI message handler
128 tuiCtx, tuiCancel := context.WithCancel(ctx)
129 var tuiWg sync.WaitGroup
132 // Set up message handling for the TUI
135 defer logging.RecoverPanic("TUI-message-handler", func() {
136 attemptTUIRecovery(program)
141 case <-tuiCtx.Done():
142 logging.Info("TUI message handler shutting down")
144 case msg, ok := <-ch:
146 logging.Info("TUI message channel closed")
154 // Cleanup function for when the program exits
159 // Cancel subscriptions first
162 // Then cancel TUI message handler
165 // Wait for TUI message handler to finish
168 logging.Info("All goroutines cleaned up")
172 result, err := program.Run()
176 logging.Error("TUI error: %v", err)
177 return fmt.Errorf("TUI error: %v", err)
180 logging.Info("TUI exited with result: %v", result)
185// attemptTUIRecovery tries to recover the TUI after a panic
186func attemptTUIRecovery(program *tea.Program) {
187 logging.Info("Attempting to recover TUI after panic")
189 // We could try to restart the TUI or gracefully exit
190 // For now, we'll just quit the program to avoid further issues
194func initMCPTools(ctx context.Context, app *app.App) {
196 defer logging.RecoverPanic("MCP-goroutine", nil)
198 // Create a context with timeout for the initial MCP tools fetch
199 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
202 // Set this up once with proper error handling
203 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
204 logging.Info("MCP message handling goroutine exiting")
208func setupSubscriber[T any](
212 subscriber func(context.Context) <-chan pubsub.Event[T],
213 outputCh chan<- tea.Msg,
218 defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
220 subCh := subscriber(ctx)
224 case event, ok := <-subCh:
226 logging.Info("subscription channel closed", "name", name)
230 var msg tea.Msg = event
233 case outputCh <- msg:
234 case <-time.After(2 * time.Second):
235 logging.Warn("message dropped due to slow consumer", "name", name)
237 logging.Info("subscription cancelled", "name", name)
241 logging.Info("subscription cancelled", "name", name)
248func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
249 ch := make(chan tea.Msg, 100)
251 wg := sync.WaitGroup{}
252 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
254 setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
255 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
258 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
259 setupSubscriber(ctx, &wg, "history", app.History.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