Diff Review

Bir ajanın dosyalardaki düzenlemelerini uygulanmadan önce gözden geçirin. Sayaçlı bir dosya ağacı, her değişikliği, her dosyayı ya da hepsini kabul veya reddetme, herhangi bir satıra ya da aralığa ajana geri giden yorumlar, birleşik ve bölünmüş görünümler, kelime düzeyinde vurgular, geri alma, akışla gelen düzenlemeler ve sohbet için bir “4 dosya düzenlendi” özeti.

Kodlama ajanları aynı anda birkaç dosyayı değiştirir ve insanların bir kısmını tutup geri kalanını atması gerekir. Diff Review her düzenlemeyi yanında dosya ağacı olan tek bir kaydırılabilir listede toplar; böylece fareyle ya da klavyeyle tek bir değişikliği, bir dosyanın tamamını veya her şeyi kabul ya da reddedebilirsiniz. DiffSummary, sohbet için “Edited 4 files +120 −34” kartıdır ve her dosyada aynı eylemlere sahiptir.

Her karar tam olarak bir değişikliğe dokunur. Karara bağlanan değişiklik, sonucu, Show ve Undo ile tek satırlık bir kalıntıya katlanır ve içindeki her değişikliğe karar verildiğinde dosya katlanır. Toplu eylemler kaç değişikliği kapsadığını söyler ve Accept all ile Reject all dahil her karar U veya ⌘Z ile geri alınabilir. “Next” her zaman henüz karar vermediğiniz sonraki değişiklik anlamına gelir. Sayılar ve ilerleme çubuğu hâlâ bekleyenlerden gelir; böylece gözden geçirme net bir “All reviewed” ile biter.

Bir satıra yorum yapmak için üzerine gelip oluktaki + tuşuna basın ya da satıra odaklanıp Enter veya C'ye basın. Bir aralığa yorum yapmak için satır numaralarını sürükleyin veya Shift ile tıklayın ya da Shift+↑↓ kullanın. Yazma alanı satırın altında küçük bir popover olarak açılır; gönderilen yorum satırda, düzenleyebileceğiniz, geri alınabilir şekilde silebileceğiniz veya daraltabileceğiniz bir konu olarak kalır ve oluktaki bir sayıyla gösterilir. Kaldırılan satırlar eski tarafta, eklenen veya değişmeyen satırlar yeni tarafta bildirilir; böylece satır numaraları her zaman dosyayla eşleşir.

Birleşik bir fark iletin ya da önceki ve sonraki içeriği verip farkı bloğun hesaplamasına izin verin. Tam içerikle, değişiklikler arasındaki değişmeyen satırları yukarı ya da aşağı 20'şer, gizli olan 20'den azsa hepsini bir seferde gösterebilirsiniz. Kaldırılan satırların oluğunda ince çizgili bir çubuk, eklenenlerde ise dolu bir çubuk bulunur; böylece fark asla renge bağlı değildir. Satırlar Code Block gibi Shiki ile vurgulanır ve bir satırdaki değişen kelimeler, iki satır yeterince benzerse işaretlenir. Yer varsa, varsayılan olarak en az 900px, düzen bölünmüş görünüme geçer. Hâlâ akan dosyalar canlı gösterilir ve henüz karara bağlanamaz. Diskte değişen dosyalar reddedilebilir ama kabul edilemez. 400 değişen satırı aşan farklar Load diff arkasında bekler ve dosya düzeyindeki eylemleri çalışmaya devam eder.

  1. Pro registry'yi components.json dosyasına ekleyin

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Token'ınızı ekleyin

    Hesap sayfanızda bir token oluşturun ve .env.local dosyasına HEXTAUI_PRO_TOKEN olarak ekleyin.

  3. Bloğu ekleyin

    pnpm dlx shadcn@latest add @hextaui-pro/diff-review

AI SDK ile

Düzenleme araç çağrılarını dosyalara dönüştürün; hâlâ akan girdi dahil: birleşik bir fark, yeni bir dosyanın içeriği ya da Claude Code'un old_string ve new_string'i. Özet sohbete, gözden geçirme yanına gider ve ikisi tek bir provider'ı paylaşır.

"use client"

import { useChat } from "@ai-sdk/react"
import { getToolName, isToolUIPart, type UIMessage } from "ai"

import type { DiffFile } from "@/components/blocks/diff-review/diff"
import { DiffReview } from "@/components/blocks/diff-review/diff-review"
import { DiffSummary } from "@/components/blocks/diff-review/diff-summary"
import { DiffReviewProvider } from "@/components/blocks/diff-review/use-diff-review"

type Input = Record<string, string | undefined>

function editsOf(messages: UIMessage[]): DiffFile[] {
  return messages.flatMap((message) =>
    message.parts.flatMap((part): DiffFile[] => {
      if (!isToolUIPart(part)) return []
      const input = (part.input ?? {}) as Input
      const streaming = part.state === "input-streaming"
      switch (getToolName(part)) {
        case "editFile":
          return input.path ? [{ path: input.path, patch: input.diff ?? "", streaming }] : []
        case "writeFile":
          return input.path ? [{ path: input.path, after: input.content ?? "", streaming }] : []
        case "Edit":
          return input.file_path
            ? [{ path: input.file_path, before: input.old_string ?? "", after: input.new_string ?? "", streaming }]
            : []
        default:
          return []
      }
    })
  )
}

export function AgentWorkspace({ onApply }: { onApply: (files: DiffFile[]) => void }) {
  const { messages } = useChat()
  const files = editsOf(messages)

  return (
    <DiffReviewProvider
      files={files}
      onDecide={({ decision, changes }) => {
        if (decision === "accepted") onApply(files.filter((file) => changes.some((change) => change.path === file.path)))
      }}
    >
      <div className="grid h-svh lg:grid-cols-[28rem_1fr]">
        <aside className="overflow-y-auto p-4">
          <DiffSummary />
        </aside>
        <DiffReview />
      </div>
    </DiffReviewProvider>
  )
}

Satır yorumlarını ajana gönderin

onComment; dosyayı, tarafı, satır numaralarını ve bir fark parçasını alır; böylece bunu takip mesajı olarak gönderebilirsiniz. sendMessage'dan promise'i döndürün; her yorum Sending… ve ardından Sent to the agent gösterir.

"use client"

import { useChat } from "@ai-sdk/react"

import type { DiffComment } from "@/components/blocks/diff-review/comments"
import type { DiffFile } from "@/components/blocks/diff-review/diff"
import { DiffReview } from "@/components/blocks/diff-review/diff-review"

function lines({ lines, side, startSide }: DiffComment) {
  const mark = (value: DiffComment["side"]) => (value === "old" ? "-" : "+")
  if (side !== startSide) return `${mark(startSide)}${lines.start} to ${mark(side)}${lines.end}`
  return lines.start === lines.end ? `${lines.start}` : `${lines.start}-${lines.end}`
}

export function ReviewWithFeedback({ files }: { files: DiffFile[] }) {
  const { sendMessage } = useChat()

  return (
    <div className="h-[36rem]">
      <DiffReview
        files={files}
        onComment={(comment) =>
          sendMessage({
            text: [
              `Feedback on ${comment.path}, ${comment.side === "old" ? "removed line" : "line"} ${lines(comment)}:`,
              "```diff",
              comment.excerpt,
              "```",
              comment.text,
            ].join("\n"),
          })
        }
      />
    </div>
  )
}

Kabul edilenleri yazın

applyDecisions, tuttuğunuz hunk'lardan her dosyayı orijinal ya da düzenlenmiş içeriği kullanarak yeniden oluşturur. Ayrıca yalnızca kabul edilen hunk'lardan bir yama döndürür ve bir dosyanın ne zaman silinmesi ya da hiç oluşturulmaması gerektiğini söyler.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"

import { applyDecisions, type DiffDecisions, type DiffFile } from "@/components/blocks/diff-review/diff"
import { DiffReview } from "@/components/blocks/diff-review/diff-review"

export function ReviewThenWrite({
  files,
  writeFile,
  deleteFile,
}: {
  files: DiffFile[]
  writeFile: (path: string, content: string) => Promise<void>
  deleteFile: (path: string) => Promise<void>
}) {
  const [decisions, setDecisions] = React.useState<DiffDecisions>({})

  const finish = async () => {
    for (const file of files) {
      const result = applyDecisions(file, decisions)
      if (result.deleted) await deleteFile(file.path)
      else if (result.content !== undefined) await writeFile(result.path, result.content)
    }
  }

  return (
    <div className="flex h-[36rem] flex-col gap-3">
      <DiffReview files={files} decisions={decisions} onDecisionsChange={setDecisions} />
      <Button onClick={finish}>Write accepted changes</Button>
    </div>
  )
}

Salt okunur bir fark

DiffView, bir dosyanın değişikliklerini aynı vurgulama, oluk çubukları ve değişmeyen satır genişletmesiyle, gözden geçirme kontrolleri olmadan gösterir. İçeriğiyle birlikte büyür; bu yüzden kaydırılabilir bir üst öğenin içine koyun.

import { DiffView } from "@/components/blocks/diff-review/diff-view"

export function VersionChanges({ previous, current }: { previous: string; current: string }) {
  return (
    <div className="flex h-96 flex-col overflow-hidden rounded-xl border">
      <div className="min-h-0 flex-1 overflow-y-auto">
        <DiffView before={previous} after={current} language="tsx" header path="app/page.tsx" />
      </div>
    </div>
  )
}

Bir git farkından

parsePatch, git diff veya düz birleşik fark çıktısını yeniden adlandırılmış, yeni, silinmiş ve ikili dosyalarla birlikte dosyalara ayırır.

import { parsePatch } from "@/components/blocks/diff-review/diff"
import { DiffReview } from "@/components/blocks/diff-review/diff-review"

export function PullRequestReview({ gitDiff }: { gitDiff: string }) {
  return (
    <div className="h-[36rem] overflow-hidden rounded-xl border">
      <DiffReview files={parsePatch(gitDiff)} defaultView="split" />
    </div>
  )
}

Yapı

Dıştan içe doğru birleştirdiğiniz parçalar.

ParçaAçıklama
DiffReviewProviderDosyaları, kararları ve geri alma geçmişini tutar; böylece sohbetteki bir özet ve bir gözden geçirme paneli eşzamanlı kalır.
DiffReviewGözden geçirme yüzeyi: araç çubuğu, dosya ağacı, değişiklikler ve ilerleme, gezinme ve toplu eylemler içeren gözden geçirme çubuğu.
DiffSummarySohbet için bir kart: sayılarla “Edited 4 files”, kabul ve reddi olan her dosya ve toplu eylemler.
DiffViewSürümler arasındaki değişiklikleri göstermek için tek bir dosyanın salt okunur farkı.
Comment threadsBir satırın ya da aralığın altındaki yorumlar; yazar, saat, iletim durumu, düzenleme, silme ve daraltma ile.
parsePatch, applyDecisionsgit diff çıktısını dosyalara okuyun ve kararları dosya içeriklerine ve yalnızca kabul edilenlerden oluşan bir yamaya geri çevirin.
useDiffReviewPaylaşılan durum, provider içinde kendi kontrollerinizi oluşturmak için.

DiffReviewProvider

DiffReview ve DiffSummary tek başlarına kullanıldığında aynı prop'ları alır.

PropTürVarsayılan
filesDeğişen dosyalar, ajanın düzenleme sırasıyla.
DiffFile[]–
decisionsDeğişiklik id'sine göre kontrollü kararlar. Kaydı olmayan değişiklikler beklemededir.
Record<string, "accepted" | "rejected">–
defaultDecisionsKontrolsüz olduğunda başlangıç kararları.
Record<string, "accepted" | "rejected">–
onDecisionsChangeGeri alma dahil her eylemden sonra sonraki kararlarla çağrılır.
(decisions) => void–
onDecideEylem başına bir kez; karar, kaynağı (change, file, all veya undo) ve etkilediği değişikliklerle çağrılır. Dosyaları yazmak veya geri almak için kullanın.
(event: DiffDecideEvent) => void–
commentsKontrollü yorumlar.
DiffComment[]–
defaultCommentsKontrolsüz olduğunda başlangıç yorumları.
DiffComment[]–
onCommentsChangeBir yorum eklendikten, düzenlendikten, silindikten veya geri yüklendikten sonra çağrılır.
(comments: DiffComment[]) => void–
onCommentBir yorum gönderildiğinde ve düzenlendiğinde aynı id ile yeniden çağrılır. Sending… ve Sent to the agent göstermek ya da reddedilirse Retry ile Couldn’t send göstermek için bir promise döndürün.
(comment: DiffComment) => unknown–
authorYeni yorumlarda gösterilir.
{ name: string; image?: string }{ name: "You" }
PropTürVarsayılan
pathDosyanın yolu. Bir ajan aynı yolu iki kez düzenlerse ikisi de listelenir.
string–
patchBu dosyanın birleşik farkı. Satır numarası olmayan hunk başlıkları, CRLF ve “No newline at end of file” sorun değildir.
string–
beforeOrijinal içerik. after ile birlikte fark sizin için hesaplanır ve değişmeyen satırlar gösterilebilir.
string–
afterDüzenlenmiş içerik. Tek başına yeni bir dosyayı tanımlar.
string–
oldPathYeniden adlandırılmış bir dosyanın önceki yolu.
string–
statusAtlanırsa içerikten çıkarılır.
"added" | "deleted" | "modified" | "renamed"–
languageShiki dil id'si. Varsayılan olarak dosya uzantısıdır.
string–
binary“Binary file not shown” gösterir ve dosyaya bir bütün olarak karar verir.
boolean–
streamingAjan bu dosyayı hâlâ yazıyor. Canlı güncellenir ve henüz karara bağlanamaz.
boolean–
staleDosya düzenlemeden sonra diskte değişti. Yeni bir fark iletene kadar kabul etme kapalıdır.
boolean–
PropTürVarsayılan
viewKontrollü düzen. Auto, fark alanı en az 900px genişliğindeyken split olur. Split, 600px altında unified'a düşer.
"auto" | "unified" | "split"–
defaultViewKontrolsüz olduğunda başlangıç düzeni.
"auto" | "unified" | "split""auto"
onViewChangeBiri bir düzen seçtiğinde çağrılır.
(view: "unified" | "split") => void–
advanceKlavyeyle karar verdikten sonra hâlâ bekleyen sonraki değişikliğe geçer.
booleantrue
largeDiffLinesBundan fazla değişen satırı olan dosyalar Load diff arkasında bekler.
number400
classNameBir yükseklik verin ya da bir flex sütununa yerleştirin. Liste içeride kayar.
string–
PropTürVarsayılan
onReviewReview gösterir ve her dosyanın gözden geçirmeyi o dosyada açmasını sağlar.
(path?: string) => void–
foldAfterDaha uzun listeleri “Show 3 more files” arkasında katlar.
number6
PropTürVarsayılan
pathYorumun ilgili olduğu dosya.
string–
sideSon satırın tarafı: kaldırılan satır için old, eklenen veya değişmeyen için new.
"old" | "new"–
startSideİlk satırın tarafı. Bir aralık kaldırılan satırlardan eklenenlere uzanıyorsa side'dan farklıdır.
"old" | "new"–
linesDosyadaki satır numaraları, startSide ve side üzerinde.
{ start: number; end: number }–
excerptSeçili satırlar “-old” ve “+new” gibi fark biçiminde; böylece ajan satır numaraları kaysa bile kodu görür.
string–
textOkuyucunun yazdığı.
string–
id, createdAt, authorYorum oluşturulduğunda doldurulur.
string, number, { name; image? }–

DiffView

diff-view.tsx dosyasından dışa aktarılır. Kendi kaydırma kapsayıcısı yoktur: içeriğiyle büyür ve değişiklik başlıkları en yakın kaydırılabilir üst öğeye yapışır; bu yüzden min-h-0 flex-1 overflow-y-auto öğesi gibi birine yerleştirin.

PropTürVarsayılan
beforeÖnceki içerik.
string–
afterSonraki içerik.
string–
patchÖnce ve sonra yerine birleşik bir fark.
string–
languageShiki dil id'si. Varsayılan olarak path uzantısıdır.
string–
pathDil ve isteğe bağlı başlık için kullanılır.
string"file"
viewSplit, 600px altında unified'a düşer.
"unified" | "split""unified"
wrapUzun satırları sarar. Split her zaman sarar.
booleanfalse
headerDeğişikliklerin üstünde yolu ve +N / −N sayılarını gösterir.
booleanfalse
classNameKök için sınıflar.
string–
PropTürVarsayılan
decisionBir karar geri alındığında beklemede.
"accepted" | "rejected" | "pending"–
sourceKişinin yaptığı.
"change" | "file" | "all" | "undo"–
changesYalnızca kararı gerçekten değişen değişiklikler.
{ file: string; path: string; id: string }[]–

applyDecisions(file, decisions, options?)

{ path, decision, content?, deleted, patch } döndürür. options.pending "accepted" değilse bekleyen değişiklikler reddedilmiş sayılır.

PropTürVarsayılan
contentGözden geçirme sonrası dosya, before veya after'dan oluşturulur. İkili dosyalar için ya da ikisi de bilinmiyorsa undefined.
string | undefined–
deletedDosya var olmamalıysa true: kabul edilmiş bir silme ya da reddedilmiş yeni bir dosya.
boolean–
patchYalnızca kabul edilen hunk'lar, git apply'ın orijinal dosyada çalışması için yeniden numaralandırılmış.
string–
TuşAction
JHâlâ bekleyen sonraki değişikliğe geçer. K geri gider.
NSonraki dosyaya geçer. P öncekine geçer.
YGeçerli değişikliği kabul eder. ⌘↵ aynısını yapar.
XGeçerli değişikliği reddeder. ⌘⌫ aynısını yapar.
⇧YGeçerli dosyadaki bekleyen her değişikliği kabul eder. ⇧X reddeder.
⌘⇧↵Hazır olan her değişikliği kabul eder. ⌘⇧⌫ reddeder.
UToplu olanlar dahil son kararı geri alır. ⌘Z aynısını yapar.
↑↓Dosya ağacında gezinir. Enter dosyaya atlar, ← ve → klasörleri katlar ve yazmak ada göre bir dosyaya atlar.
TabBir değişikliğin satırlarına girer. Her değişiklik bir duraktır ve ↑ ↓ Home End satırları arasında gezdirir; bölünmüş görünümde ← → tarafları değiştirir.
⇧↓Bir satır aralığı seçer. ⇧↑ yukarı doğru genişletir, Esc temizler.
EnterOdaktaki satır veya seçili aralık için yorum yazma alanını açar. C aynısını yapar.
⌘↵Yorumu yazma alanından gönderir. Esc kapatır ve o satır için yazdıklarınızı korur.
  • Gözden geçirme, “Review changes” adlı bir bölgedir. Her değişiklik tam adı olan bir gruptur, örneğin “Change 2 of 9, app/page.tsx, lines 40–52, 3 lines added, 1 line removed, pending”.
  • Kararlar, toplu eylemler ve geri alma, kalanla birlikte polite bir live region ile duyurulur; örneğin “Accepted change 2 of 9 in app/page.tsx. 7 changes left.”
  • Kısayollar yalnızca odak gözden geçirmenin içindeyken çalışır. Metin alanlarında yok sayılır ve dosya ağacındaki harfler ada göre dosyalara atlar. Klavyeyle karar verdikten sonra odak bekleyen sonraki değişikliğe geçer. Tıkladıktan sonra aynı yerdeki Undo düğmesine geçer.
  • Eklenen ve kaldırılan satırlar “Added:” ve “Removed:” olarak okunur; böylece renge bağımlı değildir. Değişen kelimeler yüksek karşıtlık modunda altı çizili gösterilir ve ilerleme ayrıca bir progressbar olarak sunulur.
  • Katlanmış değişiklikler etkisizdir; bu yüzden gösterene kadar Tab ve ekran okuyucular tarafından atlanır. Azaltılmış harekette katlama, sayılar ve ilerleme animasyonsuz güncellenir.
  • Her değişikliğin satırları, bir ızgara gibi, dolaşan odaklı tek bir sekme durağıdır. Odaktaki satır bir ton ve oluğunda + gösterir; zorunlu renkler modu bunun yerine bir ana hat çizer.
  • Yazma alanını açmak odağı içine taşır ve satır vurgulu kalır. Esc odağı görünür biçimde satıra döndürür. Gönderdikten ya da başka yere tıkladıktan sonra odak, odak göstergesi olmadan satıra döner; böylece sonraki ok tuşu oradan devam eder.
  • Kaldırılan ve eklenen satırlar − ve + işaretlerinin yanı sıra çizgili ya da dolu bir olluk çubuğu taşır ve çubuklar zorunlu renkler modunda görünür kalır.
  • Yorum ekleme, düzenleme, silme ve geri yükleme duyurulur; silinen yorumlar birkaç saniye boyunca geri yüklenebilir.

Şunlarla geliştirildi

Diff Review bloğunun yapıldığı ücretsiz HextaUI bileşenleri. Her biri tek başına yüklenir.

Kod

13 dosya, components/blocks/diff-review konumuna eklendi.