Skip to content
Baidak.D edited this page Sep 15, 2026 · 26 revisions

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):

review — a full-file diff with syntax highlighting

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):

review — hunks-only view

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.

What it can do

  • 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 add the 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 to HEAD (both the working tree and the index). Asks for confirmation first: only y goes 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 until r refreshes 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); pressing w collects all comments into markdown, copies them to the clipboard to feed back to Claude ("here are the comments, fix them") and clears them. s skips 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, otherwise vim in a new tab (in which case the overlay closes — a terminal editor needs a terminal).
  • Search the diff (⌘f, navigate with n / N) with match highlighting.
  • Find in Files (Cmd+Shift+F) — live project-wide git grep, see below.
  • The whole work of a branch (b) — compare with the base branch instead of HEAD: 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.

Editing in place

review — editing in the final view

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.

Autocomplete

review — autocomplete in the 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 / import line at the top of the file;
  • arguments as placeholders — where(column, value) comes with column selected: type over it, Tab moves to value, and Tab after the last one puts the caret after the call (⇧Tab goes back, Esc stops);
  • 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 press Esc.

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.

The whole work of a branch

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:

review — the whole work of a branch compared against main

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.

Several repositories in one folder

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.

review — three repositories in one tree

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.

Go to definition

⌥-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.

review — go to definition jumped into an unchanged file

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.

Find in Files

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.

review — Find in Files mode with live git grep

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

Setup

familiar enable review

Reload 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.py

Unlike 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.

Keys

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.

Working with Claude Code

Comments back to Claude

  1. Tab — go to the diff, ↑/↓ — land on a line.

  2. Enter or c — write a comment (a ● appears next to the line). Shift+Enter — a new line; the text wraps by words, Ctrl+W / Ctrl+U erase a word / everything.

  3. Go through all the spots, across different files.

  4. 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()
  5. 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.

Paths and lines

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:

  • ⌘c copies @path/to/file.py of 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+c copies @path/to/file.py#L42, or @path/to/file.py#L42-58 when 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").

What's compared with what

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".

Clone this wiki locally