package cmd import ( "encoding/json" "fmt" "html/template" "net" "net/http" "os" "plandex-cli/api" "plandex-cli/auth" "plandex-cli/lib" "plandex-cli/term" "plandex-cli/ui" "github.com/eiannone/keyboard" "github.com/fatih/color" "github.com/spf13/cobra" ) var showDiffUi bool = true var diffUiSideBySide = true var diffUiLineByLine bool var diffGit bool var fromTellMenu bool var diffsCmd = &cobra.Command{ Use: "diff", Aliases: []string{"diffs"}, Short: "Review pending changes", Run: diffs, } func init() { RootCmd.AddCommand(diffsCmd) diffsCmd.Flags().BoolVarP(&plainTextOutput, "plain", "p", false, "Output diffs in plain text with no ANSI codes") diffsCmd.Flags().BoolVar(&showDiffUi, "ui", false, "Show diffs in a browser UI") diffsCmd.Flags().BoolVar(&diffGit, "git", true, "Show diffs in git diff format") diffsCmd.Flags().BoolVarP(&diffUiSideBySide, "side", "s", true, "Show diffs UI in side-by-side view") diffsCmd.Flags().BoolVarP(&diffUiLineByLine, "line", "l", false, "Show diffs UI in line-by-line view") diffsCmd.Flags().BoolVar(&fromTellMenu, "from-tell-menu", false, "Show diffs from the tell menu") diffsCmd.Flags().MarkHidden("from-tell-menu") } func diffs(cmd *cobra.Command, args []string) { auth.MustResolveAuthWithOrg() lib.MaybeResolveProject() if lib.CurrentPlanId == "" { term.OutputNoCurrentPlanErrorAndExit() } term.StartSpinner("") if showDiffUi { diffGit = false } else if diffGit || plainTextOutput { showDiffUi = false } else { diffGit = true } diffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi) term.StopSpinner() if err != nil { term.OutputErrorAndExit("Error getting plan diffs: %v", err) return } if len(diffs) == 0 { fmt.Println("🤷♂️ No pending changes") return } if showDiffUi { getNewListener := func() net.Listener { outputFormat := "line-by-line" if diffUiSideBySide { outputFormat = "side-by-side" } else if diffUiLineByLine { outputFormat = "line-by-line" } // Properly escape the diff content for JavaScript diffJSON, err := json.Marshal(diffs) if err != nil { term.OutputErrorAndExit("Error encoding diff content: %v", err) } // Create template data data := struct { DiffContent template.JS OutputFormat string }{ DiffContent: template.JS(diffJSON), OutputFormat: outputFormat, } // Parse and execute the template tmpl, err := template.New("diff").Parse(htmlTemplate) if err != nil { term.OutputErrorAndExit("Error parsing template: %v", err) } // Use :0 to let the OS pick an available port listener, err := net.Listen("tcp", ":0") if err != nil { term.OutputErrorAndExit("Error starting server: %v", err) } // Get the actual port chosen port := listener.Addr().(*net.TCPAddr).Port // Start web server go func() { http.HandleFunc("/"+outputFormat, func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") err := tmpl.Execute(w, data) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } }) http.Serve(listener, nil) }() ui.OpenURL("Showing "+outputFormat+" diffs in your default browser...", fmt.Sprintf("http://localhost:%d/%s", port, outputFormat)) fmt.Println() return listener } listener := getNewListener() defer listener.Close() var relaunch bool for { if relaunch { listener.Close() listener = getNewListener() defer listener.Close() relaunch = false } if diffUiLineByLine { fmt.Printf("%s for side-by-side view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(s)")) } else { fmt.Printf("%s for line-by-line view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(l)")) } fmt.Printf("%s for git diff format\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(g)")) // fmt.Printf("%s to quit\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(q)")) s := "to exit menu/continue" if fromTellMenu { s = "to go back" } fmt.Printf("%s %s %s %s %s", color.New(term.ColorHiMagenta, color.Bold).Sprint("Press a hotkey,"), color.New(color.FgHiWhite, color.Bold).Sprintf("↓"), color.New(term.ColorHiMagenta, color.Bold).Sprintf("to select, or"), color.New(color.FgHiWhite, color.Bold).Sprintf("enter"), color.New(term.ColorHiMagenta, color.Bold).Sprintf("%s>", s), ) char, key, err := term.GetUserKeyInput() if err != nil { term.OutputErrorAndExit("Error getting key: %v", err) return } fmt.Println() if key == keyboard.KeyArrowDown { options := []string{} if diffUiLineByLine { options = append(options, "side-by-side") } else { options = append(options, "line-by-line") } options = append(options, "git diff") options = append(options, "exit menu") selected, err := term.SelectFromList( "Select an action", options, ) if err != nil { term.OutputErrorAndExit("Error selecting action: %v", err) return } if selected == "side-by-side" { diffUiSideBySide = true diffUiLineByLine = false relaunch = true } else if selected == "line-by-line" { diffUiSideBySide = false diffUiLineByLine = true relaunch = true } else if selected == "git diff" { showGitDiff() } else if selected == "exit menu" { fmt.Println() break } } else if string(char) == "g" { showGitDiff() } else if string(char) == "s" { diffUiSideBySide = true diffUiLineByLine = false relaunch = true } else if string(char) == "l" { diffUiSideBySide = false diffUiLineByLine = true relaunch = true } else if key == 13 || key == 10 || string(char) == "q" { // Check raw key codes for Enter/Return if term.IsRepl { fmt.Println() break } else { fmt.Println() break } } else if string(char) == "\x03" { // Ctrl+C os.Exit(0) } else { fmt.Println() term.OutputSimpleError("Invalid hotkey") fmt.Println() } } } else { if plainTextOutput { fmt.Println(diffs) } else { term.PageOutput(diffs) } fmt.Println() } } func showGitDiff() { _, err := lib.ExecPlandexCommandWithParams([]string{"diff", "--git"}, lib.ExecPlandexCommandParams{ DisableSuggestions: true, }) if err != nil { term.OutputErrorAndExit("Error showing git diff: %v", err) } } var htmlTemplate = `
`