Something went wrong. Try again.
A code review TUI that tries to reduce how many red and green squiggles you have to squint at
diff-viewer code-review llm
Something went wrong. Try again.
thresh cli.go
15 kB · 431 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432package main
import ( "context" "fmt" "io" "os" "path/filepath"
"charm.land/fang/v2" "github.com/spf13/cobra"
"secluded.site/thresh/internal/review")
type invocationOptions struct { agent bool identity string identityEnvironment bool configurationPath string configurationError error}
type invocationActorResolution struct { actor review.Actor configured bool}
func newInvocationOptions() *invocationOptions { identity, configured := os.LookupEnv("THRESH_IDENTITY") configurationPath, err := defaultConfigurationPath() return &invocationOptions{ identity: identity, identityEnvironment: configured, configurationPath: configurationPath, configurationError: err, }}
func (options *invocationOptions) addFlags(command *cobra.Command) { flags := command.PersistentFlags() flags.BoolVar(&options.agent, "agent", false, "Use agent-mode behavior for this invocation.") flags.StringVar(&options.identity, "identity", options.identity, "Display name for authored review activity.")}
func (options invocationOptions) actor(identityFlagSet bool) (review.Actor, error) { resolution, err := options.resolveActor(identityFlagSet) return resolution.actor, err}
func (options invocationOptions) resolveActor( identityFlagSet bool,) (invocationActorResolution, error) { kind := review.ActorHuman if options.agent { kind = review.ActorAgent } name, identityConfigured := options.identity, true if !identityFlagSet && !options.identityEnvironment && options.agent { name = "agent" } if !identityFlagSet && !options.identityEnvironment && !options.agent { var err error name, identityConfigured, err = options.defaultHumanIdentity() if err != nil { return invocationActorResolution{}, err } } actor, err := review.NewActor(kind, name) if err != nil { return invocationActorResolution{}, fmt.Errorf("invalid invocation identity: %w", err) } return invocationActorResolution{actor: actor, configured: identityConfigured}, nil}
func (options invocationOptions) defaultHumanIdentity() (string, bool, error) { configuration, _, err := options.loadConfiguration() if err != nil { return "", false, err } if configuration.Identity != nil { return configuration.Identity.Name, true, nil } return "user", false, nil}
func (options invocationOptions) loadConfiguration() (configurationFile, bool, error) { if options.configurationError != nil { return configurationFile{}, false, options.configurationError } return loadConfiguration(options.configurationPath)}
func main() { if executeCLI(context.Background(), os.Args[1:], os.Stdout, os.Stderr) != nil { os.Exit(1) }}
func executeCLI(ctx context.Context, args []string, stdout, stderr io.Writer) error { root := newRootCommand() root.SetArgs(args) root.SetOut(stdout) root.SetErr(stderr) return fang.Execute(ctx, root)}
func newRootCommand() *cobra.Command { diagnostics := newDiagnosticsOptions() invocation := newInvocationOptions() root := &cobra.Command{ Use: "thresh", Short: "Local-first code review with reading diffs.", Args: cobra.NoArgs, RunE: func(command *cobra.Command, _ []string) error { return command.Help() }, } invocation.addFlags(root) root.AddCommand( newDiffCommand(&diagnostics, invocation), newReviewCommand(), newRoundCommand(), newDiscussionCommand(invocation), newSkillCommand(), ) return root}
func newDiffCommand(diagnostics *diagnosticsOptions, invocation *invocationOptions) *cobra.Command { action := &diffCommand{Workers: 3, Model: os.Getenv("THRESH_MODEL")} command := &cobra.Command{ Use: "diff [revision]", Short: "Review a local revision or captured workspace changes.", Args: cobra.MaximumNArgs(1), PreRunE: action.validateHistoryFlags, RunE: func(command *cobra.Command, args []string) error { if len(args) == 1 { action.Revision = args[0] } resolution, err := invocation.resolveActor( command.Root().PersistentFlags().Changed("identity"), ) if err != nil { return err } configuration, _, err := invocation.loadConfiguration() if err != nil { return err } action.ChangeView, err = resolveChangeViewRequest( action.Individual, action.Continuous, configuration, ) if err != nil { return err } action.Abridging = resolveAbridging(action.Abridging, command.Flags().Changed("abridging"), configuration) action.configureKeymap(configuration) if err := action.configureInference(configuration, command.Flags().Changed("model")); err != nil { return err } action.ConfigurationDirectory = filepath.Dir(invocation.configurationPath) commenter := resolution.actor if !resolution.configured { commenter, err = runIdentityPrompt( command.Context(), invocation.configurationPath, command.InOrStdin(), command.OutOrStdout(), ) if err != nil { return err } } action.Commenter = commenter return runDiffWithDiagnostics(command, action, *diagnostics) }, } diagnostics.addFlags(command) flags := command.Flags() flags.StringVar(&action.VCS, "vcs", "", "Require git, jj, or hg instead of automatic discovery.") flags.StringVarP(&action.Change, "change", "c", "", "Review one Mercurial changeset against its first parent.") flags.StringVar(&action.From, "from", "", "Mercurial aggregate comparison base.") flags.StringVar(&action.To, "to", "", "Mercurial aggregate comparison head.") flags.BoolVar(&action.Staged, "staged", false, "Review captured Git index changes.") flags.BoolVar(&action.Unstaged, "unstaged", false, "Review captured tracked Git worktree changes.") flags.Uint64Var(&action.Round, "round", 0, "Open a saved round by number, without capturing source.") flags.Uint64Var(&action.Since, "since", 0, "Compare --round with its immediately preceding saved round.") flags.StringVar(&action.Discussion, "discussion", "", "Open a saved discussion's original retained context.") flags.BoolVar(&action.Individual, "individual", false, "Review changes individually.") flags.BoolVar(&action.Continuous, "continuous", false, "Review all changes continuously.") command.MarkFlagsMutuallyExclusive("individual", "continuous") flags.BoolVar(&action.Abridging, "abridging", true, "Generate abridgments. Use --abridging=false to disable.") flags.StringVar(&action.Model, "model", action.Model, "Model used to generate reading diffs.") flags.IntVar(&action.Workers, "workers", 3, "Maximum concurrent reading-diff jobs.") return command}
func (command *diffCommand) configureKeymap(configuration configurationFile) { if configuration.Keymap != nil { command.Keymap = *configuration.Keymap }}
func newReviewCommand() *cobra.Command { command := &cobra.Command{Use: "review", Short: "Inspect saved reviews.", Args: cobra.NoArgs} listAction := &reviewListCommand{} list := &cobra.Command{ Use: "list", Short: "List saved reviews.", Args: cobra.NoArgs, RunE: func(command *cobra.Command, _ []string) error { return listAction.run(command.Context(), command.OutOrStdout()) }, } list.Flags().BoolVar(&listAction.JSON, "json", false, "Write stable JSON output.") showAction := &reviewShowCommand{Repo: "."} show := &cobra.Command{ Use: "show [review-id-prefix]", Short: "Show a saved review.", Args: cobra.MaximumNArgs(1), RunE: func(command *cobra.Command, args []string) error { if len(args) == 1 { showAction.ReviewID = args[0] } return showAction.run(command.Context(), command.OutOrStdout()) }, } show.Flags().StringVar(&showAction.Repo, "repo", ".", "Repository used to select a review.") show.Flags().BoolVar(&showAction.JSON, "json", false, "Write stable JSON output.") command.AddCommand(list, show) return command}
func newDiscussionCommand(invocation *invocationOptions) *cobra.Command { command := &cobra.Command{ Use: "discussion", Short: "Manage review discussions and their comments.", Args: cobra.NoArgs, RunE: func(command *cobra.Command, _ []string) error { return command.Help() }, } listAction := &commentListCommand{Repo: "."} list := &cobra.Command{ Use: "list [review-id-prefix]", Short: "List discussions in a saved review.", Args: cobra.MaximumNArgs(1), RunE: func(command *cobra.Command, args []string) error { if len(args) == 1 { listAction.ReviewID = args[0] } return listAction.run(command.Context(), command.OutOrStdout()) }, } list.Flags().StringVar(&listAction.Repo, "repo", ".", "Repository used to select a review.") list.Flags().BoolVar(&listAction.JSON, "json", false, "Write stable JSON output.") list.Flags().BoolVar( &listAction.IncludeResolved, "include-resolved", false, "Include explicitly resolved discussions.", ) command.AddCommand( newCommentCreateCommand(invocation), newCommentDeleteCommand(invocation), newCommentEditCommand(invocation), list, newCommentLifecycleCommand(commentResolve, invocation), newCommentLifecycleCommand(commentReopen, invocation), newCommentReplyCommand(invocation), newCommentSourceCommand(), ) return command}
func newCommentDeleteCommand(invocation *invocationOptions) *cobra.Command { deletion := &commentDeleteCommand{Repo: "."} command := &cobra.Command{ Use: "delete <comment-id-prefix>", Short: "Delete one comment, not its discussion or replies.", Args: cobra.ExactArgs(1), RunE: func(command *cobra.Command, args []string) error { actor, err := invocation.actor(command.Root().PersistentFlags().Changed("identity")) if err != nil { return err } return deletion.run(command.Context(), command.OutOrStdout(), args[0], actor) }, } flags := command.Flags() flags.StringVar(&deletion.Repo, "repo", ".", "Repository used to select a review.") flags.StringVar(&deletion.ReviewID, "review", "", "Review ID prefix to select.") flags.BoolVar(&deletion.JSON, "json", false, "Write stable JSON output.") return command}
func newCommentEditCommand(invocation *invocationOptions) *cobra.Command { edit := &commentEditCommand{Repo: "."} command := &cobra.Command{ Use: "edit <comment-id-prefix> [body]", Short: "Edit one comment in a discussion.", Args: cobra.RangeArgs(1, 2), RunE: func(command *cobra.Command, args []string) error { actor, err := invocation.actor(command.Root().PersistentFlags().Changed("identity")) if err != nil { return err } bodyArgument := "" if len(args) == 2 { bodyArgument = args[1] } body, err := readCommentBody(bodyArgument, edit.File, edit.Stdin, command.InOrStdin()) if err != nil { return err } return edit.run(command.Context(), command.OutOrStdout(), args[0], body, actor) }, } flags := command.Flags() flags.StringVar(&edit.Repo, "repo", ".", "Repository used to select a review.") flags.StringVar(&edit.ReviewID, "review", "", "Review ID prefix to select.") flags.StringVar(&edit.File, "file", "", "Read the replacement body from this file.") flags.BoolVar(&edit.Stdin, "stdin", false, "Read the replacement body from standard input.") flags.BoolVar(&edit.JSON, "json", false, "Write stable JSON output.") return command}
func newCommentCreateCommand(invocation *invocationOptions) *cobra.Command { create := &commentCreateCommand{Repo: "."} command := &cobra.Command{ Use: "create [body]", Short: "Start a discussion on saved source or the whole review.", Args: cobra.MaximumNArgs(1), RunE: func(command *cobra.Command, args []string) error { actor, err := invocation.actor(command.Root().PersistentFlags().Changed("identity")) if err != nil { return err } bodyArgument := "" if len(args) == 1 { bodyArgument = args[0] } body, err := readCommentBody(bodyArgument, create.File, create.Stdin, command.InOrStdin()) if err != nil { return err } return create.run(command.Context(), command.OutOrStdout(), body, actor) }, } flags := command.Flags() flags.StringVar(&create.Repo, "repo", ".", "Repository used to select a review.") flags.StringVar(&create.ReviewID, "review", "", "Review ID prefix to select.") flags.StringVar( &create.At, "at", "", "Saved-view target: PATH, PATH:LINE, or PATH:FIRST-LAST; omit for the whole review.", ) flags.StringVar(&create.Side, "side", "", "View side for --at when not inferred: earlier or later.") flags.StringVar(&create.Unit, "change", "", "Native change selector or full saved ID to disambiguate --at.") flags.StringVar(&create.File, "file", "", "Read the comment body from this file.") flags.BoolVar(&create.Stdin, "stdin", false, "Read the comment body from standard input.") flags.BoolVar(&create.JSON, "json", false, "Write stable JSON output.") return command}
func newCommentLifecycleCommand( action commentLifecycleAction, invocation *invocationOptions,) *cobra.Command { lifecycle := &commentLifecycleCommand{Repo: ".", Action: action} command := &cobra.Command{ Use: action.commandName() + " <discussion-id-prefix>", Short: action.commandDescription(), Args: cobra.ExactArgs(1), RunE: func(command *cobra.Command, args []string) error { actor, err := invocation.actor(command.Root().PersistentFlags().Changed("identity")) if err != nil { return err } return lifecycle.run(command.Context(), command.OutOrStdout(), args[0], actor) }, } flags := command.Flags() flags.StringVar(&lifecycle.Repo, "repo", ".", "Repository used to select a review.") flags.StringVar(&lifecycle.ReviewID, "review", "", "Review ID prefix to select.") flags.BoolVar(&lifecycle.JSON, "json", false, "Write stable JSON output.") return command}
func newCommentReplyCommand(invocation *invocationOptions) *cobra.Command { reply := &commentReplyCommand{Repo: "."} command := &cobra.Command{ Use: "reply <discussion-id-prefix> [body]", Short: "Add a comment to a discussion, optionally replying to a parent comment.", Args: cobra.RangeArgs(1, 2), RunE: func(command *cobra.Command, args []string) error { reply.ParentSet = command.Flags().Changed("parent") actor, err := invocation.actor(command.Root().PersistentFlags().Changed("identity")) if err != nil { return err } bodyArgument := "" if len(args) == 2 { bodyArgument = args[1] } body, err := readCommentBody(bodyArgument, reply.File, reply.Stdin, command.InOrStdin()) if err != nil { return err } return reply.run(command.Context(), command.OutOrStdout(), args[0], body, actor) }, } flags := command.Flags() flags.StringVar(&reply.Repo, "repo", ".", "Repository used to select a review.") flags.StringVar(&reply.ReviewID, "review", "", "Review ID prefix to select.") flags.StringVar( &reply.Parent, "parent", "", "Parent comment ID prefix in this discussion; omit for a discussion-level reply.", ) flags.StringVar(&reply.File, "file", "", "Read the reply body from this file.") flags.BoolVar(&reply.Stdin, "stdin", false, "Read the reply body from standard input.") flags.BoolVar(&reply.JSON, "json", false, "Write stable JSON output.") return command}
func newSkillCommand() *cobra.Command { return &cobra.Command{ Use: "skill", Short: "Print the agent interaction skill.", Args: cobra.NoArgs, RunE: func(command *cobra.Command, _ []string) error { return writeSkill(command.OutOrStdout()) }, }}