12 tea "github.com/charmbracelet/bubbletea/v2"
13 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/db"
16 "github.com/charmbracelet/crush/internal/format"
17 "github.com/charmbracelet/crush/internal/llm/agent"
18 "github.com/charmbracelet/crush/internal/log"
19 "github.com/charmbracelet/crush/internal/pubsub"
20 "github.com/charmbracelet/crush/internal/tui"
21 "github.com/charmbracelet/crush/internal/version"
22 "github.com/charmbracelet/fang"
23 "github.com/charmbracelet/x/term"
24 "github.com/spf13/cobra"
27var rootCmd = &cobra.Command{
29 Short: "Terminal-based AI assistant for software development",
30 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
31It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
32to assist developers in writing, debugging, and understanding code directly from the terminal.`,
34 # Run in interactive mode
37 # Run with debug logging
40 # Run with debug slog.in a specific directory
41 crush -d -c /path/to/project
46 # Run a single non-interactive prompt
47 crush -p "Explain the use of context in Go"
49 # Run a single non-interactive prompt with JSON output format
50 crush -p "Explain the use of context in Go" -f json
52 RunE: func(cmd *cobra.Command, args []string) error {
54 debug, _ := cmd.Flags().GetBool("debug")
55 cwd, _ := cmd.Flags().GetString("cwd")
56 prompt, _ := cmd.Flags().GetString("prompt")
57 outputFormat, _ := cmd.Flags().GetString("output-format")
58 quiet, _ := cmd.Flags().GetBool("quiet")
60 // Validate format option
61 if !format.IsValid(outputFormat) {
62 return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText())
68 return fmt.Errorf("failed to change directory: %v", err)
74 return fmt.Errorf("failed to get current working directory: %v", err)
79 cfg, err := config.Init(cwd, debug)
84 // Create main context for the application
85 ctx, cancel := context.WithCancel(context.Background())
88 // Connect DB, this will also run migrations
89 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
94 app, err := app.New(ctx, conn, cfg)
96 slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
99 // Defer shutdown here so it runs for both interactive and non-interactive modes
102 // Initialize MCP tools early for both modes
103 initMCPTools(ctx, app, cfg)
105 prompt, err = maybePrependStdin(prompt)
107 slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
111 // Non-interactive mode
113 // Run non-interactive flow using the App method
114 return app.RunNonInteractive(ctx, prompt, outputFormat, quiet)
118 program := tea.NewProgram(
121 tea.WithKeyReleases(),
122 tea.WithUniformKeyLayout(),
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 log.RecoverPanic("TUI-message-handler", func() {
137 attemptTUIRecovery(program)
142 case <-tuiCtx.Done():
143 slog.Info("TUI message handler shutting down")
145 case msg, ok := <-ch:
147 slog.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 slog.Info("All goroutines cleaned up")
173 result, err := program.Run()
177 slog.Error(fmt.Sprintf("TUI run error: %v", err))
178 return fmt.Errorf("TUI error: %v", err)
181 slog.Info(fmt.Sprintf("TUI exited with result: %v", result))
186// attemptTUIRecovery tries to recover the TUI after a panic
187func attemptTUIRecovery(program *tea.Program) {
188 slog.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, cfg *config.Config) {
197 defer log.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, cfg)
205 slog.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 log.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
221 subCh := subscriber(ctx)
225 case event, ok := <-subCh:
227 slog.Info("subscription channel closed", "name", name)
231 var msg tea.Msg = event
234 case outputCh <- msg:
235 case <-time.After(2 * time.Second):
236 slog.Warn("message dropped due to slow consumer", "name", name)
238 slog.Info("subscription cancelled", "name", name)
242 slog.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, "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 slog.Info("Cancelling all subscriptions")
263 cancel() // Signal all goroutines to stop
265 waitCh := make(chan struct{})
267 defer log.RecoverPanic("subscription-cleanup", nil)
274 slog.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 slog.Warn("Timed out waiting for some subscription goroutines to complete")
281 return ch, cleanupFunc
285 if err := fang.Execute(
286 context.Background(),
288 fang.WithVersion(version.Version),
295 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
297 rootCmd.Flags().BoolP("help", "h", false, "Help")
298 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
299 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
301 // Add format flag with validation logic
302 rootCmd.Flags().StringP("output-format", "f", format.Text.String(),
303 "Output format for non-interactive mode (text, json)")
305 // Add quiet flag to hide spinner in non-interactive mode
306 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
308 // Register custom validation for the format flag
309 rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
310 return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp
314func maybePrependStdin(prompt string) (string, error) {
315 if term.IsTerminal(os.Stdin.Fd()) {
318 fi, err := os.Stdin.Stat()
322 if fi.Mode()&os.ModeNamedPipe == 0 {
325 bts, err := io.ReadAll(os.Stdin)
329 return string(bts) + "\n\n" + prompt, nil
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