-
Notifications
You must be signed in to change notification settings - Fork 0
Review
English · Русский
A kitten for kitty: a two-pane overlay for reviewing uncommitted git changes. On the left, a tree of changed files; on the right, a unified diff with syntax highlighting, live as you navigate.
The tree shows every kind of change at once — staged, modified, renamed and
untracked files — with per-file +/− stats; in the diff, the words that actually
changed within a line are highlighted brighter (word-diff):

a toggles the view — the whole file, or just the changed hunks with unchanged
context collapsed into ┈ separators. Enter (or a click) on a separator reveals the
next 50 hidden lines: they continue the code above the separator, while the separator
itself, the cursor and the viewport move down with them, so the next press lands on the
same spot on screen. The label says how many lines are still hidden and how many the
next press will reveal (Enter to expand when what's left fits in one step):

Plus a third view (v) — final code, the way an IDE shows it: the whole file with no
+/− signs and no removed lines; edits are marked in the gutter. Press i there and
it becomes an editor — see Editing in place.
- Shows the uncommitted changes in the working tree (vs
HEAD), untracked files included. - On the left, a tree of files (folders in bold, files colored by status
M/A/D/R). - On the right, the unified diff of the selected file: additions in green
+, deletions in red−, context with IDE-grade syntax highlighting (Pygments): functions, types,self, decorators, docstrings, f-strings — by file extension. Updates instantly as you move. - Word-diff: in a removed/added line pair, the words that actually changed are highlighted more brightly, not the whole line.
-
Go to definition — ⌥-click a symbol in the diff (or select a word and press
d) to jump to where it is defined, without leaving the overlay — see Go to definition. -
Two view modes (
a): hunks only (changes with context) or the whole file expanded, with changes marked inline. -
Final code (
v) — an IDE-style view: read the code as it will look after the merge. No+/−, no removed lines, no highlighting inside the lines — just a gutter marker (▎green — added,▎blue — modified,▔red — something was cut here,▁red — code was cut after the last line of the file). To see what exactly changed within a line, switch back to the unified view. Jumps, comments, copying and search work as in the diff, and the cursor stays on the same line when you switch. -
Edit in place (
i) — fix the code right in the final view, with undo, selection, autosave and autocomplete from the language server; nothing to switch to — see Editing in place. - A change map on the scrollbar to the right of the diff — colored ticks show where the edits are (green — added, blue — modified, red — deleted), so you can see where to scroll.
-
Jump between changes (
[/]) — across edit blocks within the diff (in both modes). -
Per-file line stats in the tree (
+added −removed), like in an IDE/GitHub. - Unversioned Files — untracked files are gathered into their own group at the bottom of the tree, collapsed by default, so a pile of new files doesn't bury the changes you came to review.
-
Stage from the tree (
+) —git addthe file under the cursor, a whole folder, or every untracked file at once (+on the group node). -
Revert changes (
-) — drop the edits of the file/folder under the cursor back toHEAD(both the working tree and the index). Asks for confirmation first: onlyygoes through. Untracked files have nothing to revert to, so they are deleted, and the prompt says so. -
Revert one block (
») — every block of changes carries a marker on the left margin of the diff, in both views; click it and just those lines go back to their committed version, while the rest of the file's edits stay. It acts right away, without a question — there is no undo, so the marker is the only thing that answers a click, and the line number next to it still opens a comment. A block that was already staged leaves the index together with the working tree, so it will not come back with the commit and the file drops out of the tree once its last block is rolled back. New files and the comparison with the base branch have no markers; a file that changed on disk since the diff was drawn is left alone untilrrefreshes it. - Sticky header: while scrolling, the enclosing function/class is pinned at the top.
-
Horizontal scroll for long lines (
h/l). - Scrollbars in both panes; the mouse wheel scrolls the pane it's over, without moving the selection.
-
Comments → markdown — you comment on lines right in the diff (multi-line:
Shift+Enter); pressingwcollects all comments into markdown, copies them to the clipboard to feed back to Claude ("here are the comments, fix them") and clears them.sskips the clipboard: it closes the overlay and pastes the comments straight into the window underneath — usually the Claude prompt. Closes the review → edit loop. -
Refresh (
r) — rescan changes without reopening the overlay (handy while Claude is still editing files). -
Open in editor (
e) — open the current file at the visible line. The editor is chosen by project config:.idea/→ JetBrains (PhpStorm/IDEA/PyCharm/…),.vscode/→ VS Code,.cursor/→ Cursor,.zed/→ Zed — the whole project opens focused on the line, and the overlay stays open. If there's no config —$VISUAL/$EDITOR, otherwisevimin a new tab (in which case the overlay closes — a terminal editor needs a terminal). -
Search the diff (
⌘f, navigate withn/N) with match highlighting. -
Find in Files (
Cmd+Shift+F) — live project-widegit grep, see below. -
The whole work of a branch (
b) — compare with the base branch instead ofHEAD: committed and uncommitted changes in one diff, see below. - Filter the tree by file name (
f), Russian keyboard layout for shortcuts.
The project folder and git root are determined from the cwd of the window the hotkey
was pressed in. If that folder is not a repository but holds several, review shows them
all — see Several repositories in one folder.

The final view doubles as an editor. Press i in the diff (in the tree it starts at the
top visible line); from the unified diff i switches to the final view first and keeps
the line. A thin blinking caret appears between the characters, as in an IDE, the footer
turns into [edit], and typing goes into the file —
letters of any layout are text now, not commands. The rest of the view stays: syntax
highlighting, the gutter markers and the change map follow each keystroke, ⌥-click still
goes to a definition (on code you have not saved, too), and ⌘f still searches —
Enter on a match puts the caret on it and selects it.
Saving is automatic. Esc writes the file and returns to the normal final view, and so
does leaving the file — a click in the tree, ⌃o, ⌥-click into another file, ⌘⇧f — or
quitting with ⌃c / ⌘w. ⌘s saves without leaving. Before writing, review checks the
file on disk: if it changed since you started (Claude is still at work), you are asked
instead of it being overwritten — y overwrites, d discards your edit (it goes to the
clipboard) and shows the file from disk, any other key keeps you editing.
While editing, a click on a block's » marker reverts that block in the editor, not on
disk — it is an ordinary edit, and ⌘z brings it back. Comments stay with their lines
when you insert or delete lines above them. After saving, the tree is rescanned: a file
you edited back to its committed version drops out of the tree but stays on screen, and a
file you reached with go-to-definition and changed shows up in it.
Editing is review-only: a commit in log and Find in Files stay read-only. A file
outside the repository (a go-to-definition target in the stdlib, say) is read-only too,
and so are binary, non-UTF-8 and CRLF files and files over 400 KB — i says why, and e
opens them in your editor.

The editor completes from the same language servers as
go-to-definition. A list opens under the word once you have typed two letters of a name,
and at once after a trigger character of the server — . in Python, ->, ::, $ and
\ in PHP; ⌃Space opens it by hand (⌥Esc does the same, in case macOS takes ⌃Space
to switch the input source). The list follows your typing without asking the server
again, in the order JetBrains IDEs use: the case of the first letter counts (Use is
the class User, not the function use_soap_error_handler), what is already in scope
comes before auto-imports, items you picked recently and names used in the file go first,
prefixes and camel humps (gTF finds getTextField) before scattered letters. Matched
letters are bold, the type or module is dimmed on the right. ↑ / ↓ pick, Enter or Tab insert,
Esc closes the list — the second Esc leaves the editor as usual.
The list stays compact, as in JetBrains IDEs: name, parameters and type. ⌃Space with
the list open shows the documentation of the selected item beside it — signature and
description, on the right or on the left, wherever there is room — and hides it again.
What an item inserts is up to the server, and review
applies all of it as one edit that a single ⌘z takes back:
-
auto-import — picking a class that is not imported yet adds its
use/importline at the top of the file; -
arguments as placeholders —
where(column, value)comes withcolumnselected: type over it,Tabmoves tovalue, andTabafter the last one puts the caret after the call (⇧Tabgoes back,Escstops); -
the signature — inside a call a line above the code shows the parameters, with the
one being typed underlined; it follows
,and the arrows and hides when you leave the call or pressEsc.
The server answers in the background, so typing never waits for it. While it is still
starting or indexing, and for files it does not know (.txt, config files), the list offers
names from the file itself. No server installed for the language — the list is just these
words; familiar lsp install <language> brings the rest.
By default review shows the working tree: what is not committed yet. Press b and
it compares against the branch you diverged from instead — everything the branch
has done, committed and uncommitted alike, as one diff. Here web is on
feature/moon-widget: main.ts and moon.ts come from the branch's commits,
styles.css is not committed yet, and all three sit in one list:

The base is whatever origin/HEAD points at, otherwise the first of
main, master, develop that exists; the comparison starts at the point the
branch diverged (git merge-base), so commits that landed on the base afterwards
do not pollute the diff. On the base branch itself there is nothing to compare
with, and the kitten says so.
In a folder of several repositories each one uses its own base, and a repository that sits on its base keeps showing its working tree — so the screen stays useful even when only one project is on a feature branch.
Staging (+) and reverting (-, including the » markers in the diff) are held back
while comparing: they act on the working tree, and in this view it is not obvious what
would be touched. b switches back.
Open review from a folder that holds independent repositories — say ~/work with
api, web and infra inside — and all of them show up in one tree. Repositories are
the top level: each row carries its branch, how many files changed and the total
+N −M; a repository with nothing to show gets no row at all.

Everything below the repository row belongs to that repository. + stages, - reverts
and the diff reads exactly the repository the file came from, so two files with the same
relative path in different repositories never mix — their marks and comments stay apart
too. Copied mentions carry the prefix (@api/sundial/core.py), which is what Claude Code
needs to open them from the folder you launched the kitten in.
R focuses one repository: pick it by the key next to it — digits first, then letters
once they run out — and the tree collapses to it alone, as if it were the only one.
Esc releases the focus, 0 in the menu brings everyone back.
Inside an ordinary repository R keeps its old meaning — refresh.
Repositories are looked for two levels deep, skipping hidden folders and artifacts
(node_modules, vendor, …); a repository nested inside one already found is left to
git itself. At most 24 are shown, and the kitten says so when it finds more.
Find in Files searches every repository in view (or just the focused one), and go-to-definition starts a language server per repository, lazily.
⌥-click a symbol in the diff — or select a word and press d — to jump to where
it is defined. The jump happens inside the viewer with a back stack (⌃o to
return): a definition in a changed file opens its diff, a definition in an
unchanged file opens as the file itself (no comments or staging there, but i
edits it). When a symbol has several definitions a picker
lists them (1–9 or click to choose); Esc or a click outside the list
closes it, and a digit with no candidate behind it leaves it open.

Definitions come from a language server, so the answer knows the language:
inheritance, traits, imports and return types. $this->belongsTo(...) in a
Laravel model lands in the trait that declares it — inside vendor/, which
.gitignore hides from git entirely — and ->withTrashed() chained after it
resolves through the type the previous call returned.
Targets outside the repository (a framework class in vendor/, a module in the
standard library) open read-only by their absolute path.
The server starts as soon as a file is on screen, not on the first click, so it
indexes while you read the diff. Until it is ready the footer shows progress —
a percentage when the server reports one, otherwise elapsed time and the growing
index cache. The index is kept on disk (~/.cache/familiar/lsp/index), so only
the very first run on a project is slow; later ones start in well under a
second. Nothing stays running in the background: servers live exactly as long as
the overlay.
Two cases cannot be answered by the position on screen. A removed line has
no document any server holds, and browsing a commit in log shows text that
is not what is on disk — feeding that to the server would poison its index.
Browsing a commit therefore resolves against the working-tree version of the
same file, carrying the line number over, so the answer is still exact. When
even that is impossible — the file is gone, or the identifier moved — a
project-wide symbol search takes over and its results land in the same picker. No editor does better here — VS Code has had the same gap in
its diff view since 2017.
Servers are installed per language:
familiar lsp status # what is configured and what is found
familiar lsp install php # intelephense, pyright, gopls, …
familiar lsp warm php # index a big project up front
familiar enable review offers to install what is missing. A language with no
server gets no navigation at all — familiar lsp status <language> prints a
registry block to copy into ~/.config/familiar/lsp.conf. See
Installation for the registry and its three
levels.
⌘-click is not possible: the terminal mouse protocol carries only Shift/Alt/Ctrl, never Cmd, so the trigger is ⌥-click. The pointer turns into a hand over identifiers you can jump to while ⌥ is held.
Cmd+Shift+F inside the overlay switches it into an IDE-style search across
the whole project (outside the overlay the hotkey does nothing — the mode
lives only in review). The query line is live: results appear as you type — git grep under the
hood, so .gitignore is respected, untracked files are seen, binaries are
skipped. Smart-case: an all-lowercase query is case-insensitive, any capital
letter makes it exact; x turns the query into a POSIX extended regex (a bad
pattern shows git's error instead of "no matches").
On the left — files with match counts (noisy folders stay hidden until u),
on the right — the file itself with syntax highlighting and all matches;
n/N and [/] jump between them, a cap of 2000 matches keeps huge result
sets responsive. e opens the editor at the match, ⌘c / ⌘⇧c copy the code
or a @path#L42 reference for a Claude prompt. Enter on a match opens the
file through review's own navigation: a changed file lands in its diff with
the full review toolset (comments, staging, go to definition), any other
opens as a plain file like go-to-definition, and ⌃o goes back. Cmd+Shift+F
inside the mode reopens the query line with the current query — searching
for the next thing takes the same shortcut that started the search. Esc
returns to the review exactly where you left it; the search pane itself is
read-only.

Keys (Find in Files)
| Key | Action |
|---|---|
| typing | live search from 2 characters |
← →, Home / End
|
move the caret (insertion, Backspace and forward Delete work at the caret) |
Enter |
in the query line — keep the query; on a file — preview at the match; on a match line — open the file in review (⌃o back) |
⌘f, ⌘⇧f
|
edit the query (⌘⇧f from the query line leaves the mode) |
n / N, [ / ]
|
previous / next match |
x |
regex mode on/off |
r |
rescan (rerun the search) |
u |
show/hide noisy folders |
⌘c / ⌘⇧c
|
copy the code / @path#L42
|
e |
open the editor at the current match |
Esc |
cancel the query edit → back to the tree → leave the mode |
q ⌃c
|
quit the overlay |
familiar enable reviewReload the config with Cmd+Ctrl+, (macOS) or restart kitty. Open:
cmd+shift+r; cmd+shift+f inside the overlay switches to Find in Files.
Minimal fallback — a manual map in ~/.config/kitty/kitty.conf (or an
included file):
map cmd+shift+r kitten /path/to/familiar/plugins/review.pyUnlike familiar enable, this bare map lacks the toggle-to-close behavior, the
guard against re-opening the overlay on top of itself, the full-screen open
from a split (the stack-layout switch), the Cyrillic key duplicates, and the
cmd+c / cmd+shift+c / cmd+f / cmd+shift+f pass-through for
copying, search and Find in Files inside the overlay.
Two focus areas: the tree (left, navigate files) and the diff (right, cursor over
lines for comments). Switch with Tab or the arrows ← (tree) / → (diff).
Mouse: click a file in the tree to select it; click a folder to select it, click it
again to fold/unfold; ⇧-click a tree row to mark the whole range from the anchor,
⌥-click one to toggle the mark on that file or folder (the cursor stays put);
click a diff line to place the cursor, double-click a word to select just that word
(for copy); click a line number to comment on that line; click the » marker on the
left margin of a block to revert that block; ⌥-click a symbol — go to
its definition; click the ┈ separator to reveal the next 50 hidden lines.
(Text in the diff is selected by dragging the mouse and copied with ⌘c; with the
terminal config enabled, Shift goes to the tree marks, not to kitty's own selection.)
Tree focus
| Key | Action |
|---|---|
↑/↓ |
navigate files (the diff on the right updates) |
⇧↑/↓ |
mark files (multi-select): paints the range from the anchor |
g / G
|
first / last file |
Enter Space
|
collapse/expand folder |
→ Tab
|
go to the diff (cursor over lines) |
+ |
git add the file / folder / all Unversioned Files under the cursor |
- |
revert changes to HEAD (new files are deleted); asks y to confirm |
r |
rescan changes (refresh) |
u |
show/hide noisy folders (.idea, node_modules, __pycache__, …) |
f |
filter the tree by file name |
R |
focus one repository (only when the folder holds several) |
b |
compare with the base branch: the whole work of the branch |
⌘f |
search the diff |
⌘⇧f |
Find in Files across the project (see below) |
Esc |
clear the marks; then the applied filter; with neither — ask to close the overlay |
q ⌃c
|
quit |
Diff focus (→/Tab from the tree; ←/Tab/Esc — back to the tree)
| Key | Action |
|---|---|
↑/↓ |
move cursor over diff lines |
g / G
|
to the start / end of the diff |
Enter |
on the ┈ separator — reveal the next 50 hidden context lines (the label says how many are left) |
Enter / c
|
comment on the line under the cursor (empty — delete one) |
{ / }
|
jump to the previous / next comment (●) |
w |
collect all comments into markdown, copy to the clipboard and clear them |
s |
collect the comments the same way, close the overlay and paste them into the window underneath |
x |
delete all comments |
[ / ]
|
previous / next change — shown with context, focus moves into the diff |
d |
go to the definition of the selected word (also ⌥-click a symbol) |
⌃o |
back after a go-to-definition jump |
i |
edit the file right here — see Editing in place |
Edit mode (i; Esc — save and back)
| Key | Action |
|---|---|
any text, ⌘v
|
typed / pasted into the file |
← → ↑ ↓, PgUp PgDn
|
move the caret |
⌥← / ⌥→
|
by words |
Home / End, ⌘← / ⌘→
|
to the line start (first press — to the code, second — to column 0) / end |
⌘↑ / ⌘↓
|
to the start / end of the file |
⇧ + a move |
select; with the mouse — drag, double-click selects a word |
Enter |
new line, keeping the indent |
Tab / ⇧Tab
|
indent / outdent (every selected line) |
Backspace / Delete
|
erase before / after the caret |
⌥⌫ / ⌘⌫
|
erase the word before the caret / to the line start |
⌘z / ⌘⇧z
|
undo / redo |
⌘a · ⌘c · ⌘x
|
select all · copy · cut (without a selection — the whole line) |
⌘s |
save and keep editing |
⌘f |
search; Enter selects the match |
⌃Space (or ⌥Esc) |
open the completion list; with the list open — show / hide the documentation |
↑ ↓, PgUp PgDn · Enter / Tab
|
in the list: pick · insert |
Tab / ⇧Tab
|
after inserting a function: next / previous argument |
Esc |
close the list, then the signature; drop the selection; then save and leave |
While typing (comment / filter / search / find)
| Key | Action |
|---|---|
Enter |
save (in a comment: an empty text deletes it) |
Shift+Enter |
new line — comments are multi-line and wrap by words |
← →, Home / End
|
move the caret — insertion happens at the caret |
Backspace / Delete
|
erase before / after the caret |
Ctrl+W |
erase the word before the caret |
Ctrl+U |
erase the whole text |
Esc |
cancel |
Common (in both focus areas)
| Key | Action |
|---|---|
PgUp PgDn
|
scroll the diff |
h / l
|
horizontal scroll of the diff (long lines) |
a |
view mode: hunks only ↔ whole file |
v |
pane view: unified diff ↔ final code (IDE-style) |
⌘f, n/N
|
search the diff and jump between matches |
⌘c |
copy: in the tree — @path of the file/folder (with marks — of every marked file), in the diff — the selection / line under the cursor |
⌘shift+c |
copy @path#L42 (in the tree — @path) |
e |
open the file in the project IDE (.idea/.vscode/.cursor/.zed) or $EDITOR
|
Esc at the bottom of the cascade (the tree, no marks, no filter applied) doesn't
close the overlay silently: a centered kitty-style dialog asks first — y / Enter /
a click on Yes closes, n / Esc / No keeps it open, ←/→/Tab switch the
buttons. ⌃c quits at once even over the dialog; q works only while the dialog
isn't shown.
File statuses (colored as in an IDE): A added — green, M modified — blue,
D deleted — gray, R renamed — cyan, ? untracked (new, not yet in git) —
red (the file is shown but marked as not added to git). A file staged as
new and then deleted from disk is not shown at all — relative to HEAD it is
not a change.
Untracked files are gathered into an Unversioned Files group at the bottom of the
tree, collapsed by default. + on the group node stages all of them at once; + on a
file or a folder stages just that. The hint only shows up when there is actually
something to add — an already staged file offers nothing.
- reverts instead: the file goes back to its HEAD version, in the working tree and in
the index alike, and an untracked file is deleted from disk — irreversibly, git has no
copy of it. Nothing happens until you press y; Enter, Esc and every other key
cancel.
Noisy IDE folders (.idea, .vscode, node_modules, __pycache__, venv, etc.) are
hidden by default — as in an IDE; u shows them (and says how many). They are never
staged by + while hidden. Build-artifact names that source folders also use
(vendor, dist, build, target, coverage) only count as noise at the root of the
repository, so vendor/laravel/… stays hidden while Laravel's
resources/views/vendor/… is shown.
-
Tab— go to the diff,↑/↓— land on a line. -
Enterorc— write a comment (a●appears next to the line).Shift+Enter— a new line; the text wraps by words,Ctrl+W/Ctrl+Uerase a word / everything. -
Go through all the spots, across different files.
-
w— all comments are collected into markdown, copied to the clipboard and cleared from the diff:# Review comments ## app/Http/Controllers/UserController.php - **L42** `return $user->save();` no permission check, add authorize()
-
Paste (
Cmd+V) into the Claude chat: "here are the review comments, fix them."
s does steps 4–5 in one go: the overlay closes and the comments are pasted straight
into the window it was opened over — where Claude is usually waiting. The paste is
bracketed, so a multi-line review lands as one block instead of being submitted line
by line.
Besides collecting comments, the diff is a quick way to point Claude Code at a specific spot in the code. Both keys copy an @-mention with a path relative to the repository root — the form Claude Code expects:
-
⌘ccopies@path/to/file.pyof the selected file, or@path/to/dir/of a folder (tree focus); in the diff it copies the selection / line under the cursor as code. -
⌘shift+ccopies@path/to/file.py#L42, or@path/to/file.py#L42-58when a range of lines is selected with the mouse.
Claude Code resolves @path against the directory it was started in, so this works when
you run claude from the project root. Land on a line in the diff → ⌘shift+c → Cmd+V
into the prompt — no need to describe the spot in words ("in such-and-such file, somewhere
near that function").
The file list is git status (plus untracked); each file's diff is the version in
HEAD ("before") against the file on disk ("after"). It answers the question "what
have I changed since the last commit".
For untracked files and for a repository without commits the "before" side is empty: the whole file shows up as added.
With b the "before" side becomes the point the branch diverged from its base
(git merge-base) instead of HEAD, and the list is git diff <base> plus
untracked — the question turns into "what has this branch done so far".
English
Русский