diff --git a/AGENTS.md b/AGENTS.md
index 8375eba..b46a0dd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,6 +11,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- Codes: `qrcode` (QR) and `barcoders` (Code 128, EAN-13), both MIT OR Apache-2.0, default features off.
- macOS extras (tray icon, Dock badge) call AppKit through `cocoa` 0.26 and `objc` 0.2, the crates gpui already links.
- Terminal: `alacritty_terminal` 0.26 (Apache-2.0, default features off) for the grid, its parser and the pseudo-terminal; `futures` carries its events.
+- Diffs: `similar` 3.2 (Apache-2.0) for line and word diffs and three-way merges; its `unicode` feature splits words at punctuation.
- Gallery: `examples/gallery`. Website: `frontend/` (Vite 8, pnpm), built after the components.
## Commands
diff --git a/Cargo.lock b/Cargo.lock
index 8e9f0f2..b8b9883 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -762,6 +762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
dependencies = [
"memchr",
+ "regex-automata",
"serde_core",
]
@@ -1603,6 +1604,7 @@ dependencies = [
"rustix 1.1.5",
"serde",
"serde_json",
+ "similar",
"smallvec",
"unicode-segmentation",
]
@@ -5327,6 +5329,16 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+[[package]]
+name = "similar"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f66ca1f7aca2474dc10c942eb22feffc897735f54cd1db90138c2fddb490987"
+dependencies = [
+ "bstr",
+ "unicode-segmentation",
+]
+
[[package]]
name = "simplecss"
version = "0.2.2"
diff --git a/Cargo.toml b/Cargo.toml
index 14e36da..37acfac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ regex = "1"
rust-embed = "8.12"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+similar = { version = "3.2", default-features = false, features = ["std", "text", "inline", "unicode"] }
smallvec = "1"
unicode-segmentation = "1"
diff --git a/TASKS.md b/TASKS.md
index 7285478..bfd1707 100644
--- a/TASKS.md
+++ b/TASKS.md
@@ -242,3 +242,4 @@ Per-component lines live in `tasks/`. Tags there:
| T18 | 1 | FAIL | a missing folder ran in the app's own; combining marks dropped; Tab skipped the terminal; its wheel scrolled the page too; clear kept find matches; New bash reused the old shell; the dark script typed into the replay |
| T18 | 2 | FAIL | a folder it could not enter still started the shell elsewhere; links read cluster cells as spaces |
| T18 | 3 | PASS | |
+| T19 | 1 | — | pending review |
diff --git a/assets/icons/git-pull-request-closed.svg b/assets/icons/git-pull-request-closed.svg
new file mode 100644
index 0000000..1b34763
--- /dev/null
+++ b/assets/icons/git-pull-request-closed.svg
@@ -0,0 +1,20 @@
+
+
diff --git a/assets/icons/git-pull-request-draft.svg b/assets/icons/git-pull-request-draft.svg
new file mode 100644
index 0000000..94ffe9f
--- /dev/null
+++ b/assets/icons/git-pull-request-draft.svg
@@ -0,0 +1,19 @@
+
+
diff --git a/examples/gallery/pages/git/diffs.rs b/examples/gallery/pages/git/diffs.rs
new file mode 100644
index 0000000..658f11f
--- /dev/null
+++ b/examples/gallery/pages/git/diffs.rs
@@ -0,0 +1,147 @@
+use std::rc::Rc;
+
+use ely_gpui_component::{
+ git::{ConflictResolver, DiffLayout, DiffViewer, Take, ThreeWayMerge, regions},
+ theme::{ActiveTheme, Radius},
+};
+use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
+
+use crate::ui::{keep, section, set};
+
+const OLD: &str = r#"use std::collections::HashMap;
+
+/// A palette of named colors.
+pub struct Palette {
+ colors: HashMap,
+ fallback: Color,
+}
+
+impl Palette {
+ pub fn new(fallback: Color) -> Self {
+ Self { colors: HashMap::new(), fallback }
+ }
+
+ pub fn insert(&mut self, name: &str, color: Color) {
+ self.colors.insert(name.to_string(), color);
+ }
+
+ pub fn len(&self) -> usize {
+ self.colors.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.colors.is_empty()
+ }
+
+ /// The color for `name`.
+ pub fn lookup(&self, name: &str) -> Color {
+ self.colors[name]
+ }
+}
+"#;
+
+const NEW: &str = r#"use std::collections::BTreeMap;
+
+/// A palette of named colors, in a steady order.
+pub struct Palette {
+ colors: BTreeMap,
+ fallback: Color,
+}
+
+impl Palette {
+ pub fn new(fallback: Color) -> Self {
+ Self { colors: BTreeMap::new(), fallback }
+ }
+
+ pub fn insert(&mut self, name: &str, color: Color) {
+ self.colors.insert(name.to_string(), color);
+ }
+
+ pub fn len(&self) -> usize {
+ self.colors.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.colors.is_empty()
+ }
+
+ /// The color for `name`, blended toward white by `lift`.
+ pub fn lookup(&self, name: &str, lift: f32) -> Color {
+ let base = self.colors.get(name).copied().unwrap_or(self.fallback);
+ base.lift(lift)
+ }
+}
+"#;
+
+pub fn viewer(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
+ let layout = keep("git-layout", || DiffLayout::Unified, window, cx);
+ let open = keep("git-open", Vec::::new, window, cx);
+ let (now_layout, now_open) = (*layout.read(cx), open.read(cx).clone());
+ let (flip, unfold) = (layout.clone(), open.clone());
+ let theme = cx.theme();
+ section(
+ "DiffViewer / InlineDiff",
+ "One file's changes, unified or side by side: each change in its context with old and new numbers, the words that changed washed, the stretches between folded until pressed. InlineDiff inside an editor is CodeEditor::set_diff.",
+ cx,
+ )
+ .child(
+ div()
+ .w(px(840.))
+ .h(px(360.))
+ .rounded(theme.radius(Radius::Md))
+ .border_1()
+ .border_color(theme.colors.border)
+ .overflow_hidden()
+ .child(
+ DiffViewer::new("git-diff", "src/palette.rs", OLD, NEW)
+ .layout(now_layout)
+ .open(now_open)
+ .on_layout(move |next, _, cx| set(&flip, next, cx))
+ .on_open(move |stretch, _, cx| {
+ let mut next = unfold.read(cx).clone();
+ next.push(stretch);
+ set(&unfold, next, cx)
+ }),
+ ),
+ )
+}
+
+const BASE: &str =
+ "fn lift(color: Color) -> Color {\n color.mix(WHITE, 0.5)\n}\n\nfn keep() {}\n";
+const OURS: &str =
+ "fn lift(color: Color) -> Color {\n color.mix(WHITE, 0.6)\n}\n\nfn keep() {}\n";
+const THEIRS: &str = "fn lift(color: Color) -> Color {\n color.mix(WHITE, LIFT)\n}\n\nfn keep() { log::info!(\"kept\") }\n";
+
+const MARKED: &str = "fn lift(color: Color) -> Color {\n<<<<<<< HEAD\n color.mix(WHITE, 0.6)\n=======\n color.mix(WHITE, LIFT)\n>>>>>>> feature/lift\n}\n\nfn keep() { log::info!(\"kept\") }\n";
+
+pub fn merges(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
+ let takes = keep("git-takes", Vec::