feat(review): add --preview flag to show files before running LLM review

This commit is contained in:
kite 2026-05-26 23:06:47 +08:00
parent 834a4e7e3f
commit e71fa597d8
5 changed files with 226 additions and 40 deletions

View file

@ -105,6 +105,7 @@ type reviewOptions struct {
background string // --background: optional requirement context
concurrency int
perFileTimeout int
preview bool
showHelp bool
}
@ -124,6 +125,7 @@ func parseReviewFlags(args []string) (reviewOptions, error) {
a.IntVar(&opts.perFileTimeout, "timeout", 10, "concurrent task timeout in minutes")
a.StringVar(&opts.audience, "audience", "human", "output audience: human (show progress) or agent (summary only)")
a.StringVarP(&opts.background, "background", "b", "", "optional requirement/business context for the review")
a.BoolVarP(&opts.preview, "preview", "p", false, "preview which files will be reviewed without running the LLM")
if err := a.Parse(args); err != nil {
return opts, fmt.Errorf("parse flags: %w", err)
@ -183,6 +185,10 @@ Examples:
# Agent mode (summary only, no progress lines)
ocr review --audience agent
# Preview which files will be reviewed
ocr review --preview
ocr review -c abc123 -p
Flags:
--audience string output audience: human (show progress) or agent (summary only) (default "human")
-b, --background string optional requirement/business context for the review
@ -190,6 +196,7 @@ Flags:
-f, --format string output format: text or json (default "text")
--concurrency int max concurrent file reviews (default 8)
--from string source ref to start diff from (e.g., 'main')
-p, --preview preview which files will be reviewed without running the LLM
--repo string root directory of the git repository (default: current dir)
--rule string path to JSON file with system review rules
--timeout int concurrent task timeout in minutes (default 10)

View file

@ -162,9 +162,9 @@ func buildDiffLines(comment model.LlmComment) []suggestdiff.DiffLine {
}
type jsonOutput struct {
Status string `json:"status"`
Message string `json:"message,omitempty"`
Comments []model.LlmComment `json:"comments"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
Comments []model.LlmComment `json:"comments"`
Warnings []agent.AgentWarning `json:"warnings,omitempty"`
}
@ -216,3 +216,65 @@ func outputJSONNoFiles() error {
enc.SetIndent("", " ")
return enc.Encode(out)
}
func outputPreviewText(p *agent.DiffPreview) {
if p.TotalFiles == 0 {
fmt.Println("No files changed.")
return
}
maxPathLen := 0
for _, e := range p.Entries {
if len(e.Path) > maxPathLen {
maxPathLen = len(e.Path)
}
}
if maxPathLen < 20 {
maxPathLen = 20
}
pathFmt := fmt.Sprintf("%%-%ds", maxPathLen)
fmt.Printf("\nPreview: %d file(s) changed | \033[32m+%d\033[0m \033[31m-%d\033[0m\n",
p.TotalFiles, p.TotalInsertions, p.TotalDeletions)
if p.ReviewableCount > 0 {
fmt.Printf("\n\033[1mWill review (%d):\033[0m\n", p.ReviewableCount)
for _, e := range p.Entries {
if !e.WillReview {
continue
}
fmt.Printf(" %s "+pathFmt+" \033[32m+%-4d\033[0m \033[31m-%-4d\033[0m\n",
statusBadge(e.Status), e.Path, e.Insertions, e.Deletions)
}
}
if p.ExcludedCount > 0 {
fmt.Printf("\n\033[1mExcluded from review (%d):\033[0m\n", p.ExcludedCount)
for _, e := range p.Entries {
if e.WillReview {
continue
}
fmt.Printf(" %s "+pathFmt+" \033[2m(%s)\033[0m\n",
statusBadge(e.Status), e.Path, e.ExcludeReason)
}
}
fmt.Println()
}
func statusBadge(status string) string {
switch status {
case "added":
return "\033[32m[A]\033[0m"
case "modified":
return "\033[33m[M]\033[0m"
case "deleted":
return "\033[31m[D]\033[0m"
case "renamed":
return "\033[36m[R]\033[0m"
case "binary":
return "\033[35m[B]\033[0m"
default:
return "[?]"
}
}

View file

@ -50,6 +50,10 @@ func runReview(args []string) error {
return fmt.Errorf("load rules: %w", err)
}
if opts.preview {
return runPreview(repoDir, opts, fileFilter)
}
toolEntries, err := toolsconfig.Load(opts.toolConfigPath)
if err != nil {
return fmt.Errorf("load tools: %w", err)
@ -198,6 +202,24 @@ func requireGitRepo(dir string) error {
return nil
}
func runPreview(repoDir string, opts reviewOptions, fileFilter *rules.FileFilter) error {
ag := agent.New(agent.Args{
RepoDir: repoDir,
From: opts.from,
To: opts.to,
Commit: opts.commit,
FileFilter: fileFilter,
})
preview, err := ag.Preview()
if err != nil {
return fmt.Errorf("preview failed: %w", err)
}
outputPreviewText(preview)
return nil
}
func buildToolRegistry(collector *tool.CommentCollector, fr *tool.FileReader, diffMap map[string]string) tool.Registry {
reg := tool.NewRegistry()
reg.Register(tool.NewFileRead(fr))