Introduction

Bad Juju is an LSP-powered, editor-agnostic frontend for the Jujutsu VCS. Instead of inventing a custom UI, Bad Juju lets you drive Jujutsu from any editor that speaks the Language Server Protocol — which today means most of them.

The core idea: everything is a text file. Want to see the status of your working copy? Bad Juju writes it to .jj/badjuju/status.jujutsu and your editor opens it. Want to edit a commit message? You open a buffer and edit it. Want to squash a hunk between two commits? The squash window is, again, just a buffer you edit and save.

This means you can:

  • Use your editor's keybindings, motions, and search — because the views are real text, you can grep them, copy from them, jump around with the same shortcuts you already know.
  • Plug Bad Juju into any LSP-capable editor. Neovim, VS Code, Helix, and Emacs all have first-party integrations in this repo; any other editor that supports LSP code actions can drive Bad Juju too.
  • Avoid context-switching. No separate Magit-style window manager, no lazygit overlay, no IDE tool window with its own keyboard conventions. Bad Juju lives inside the editor you're already in.

If you've used Magit, Fugitive, or Lazygit, the workflows here will feel familiar — single-key actions on a status buffer, log views you can navigate, hunk-level squashing — just translated to Jujutsu's mental model and rendered as plain text inside your editor.

What you'll find in this guide

  • Getting Started walks through installing the server and pointing your editor at it.
  • Usage covers the everyday workflows — opening the status window, creating new revisions, viewing diffs, and rewriting history.
  • Clients collects the editor-specific notes for each supported frontend, including the per-buffer keymaps.
  • Reference is the detailed tour of each buffer Bad Juju produces, for when you want to know exactly what the JJ: lines mean or which commands a buffer responds to.

Status

Bad Juju is under active development. Expect rough edges — but also expect that the foundations (status, log, diff, describe, squash, hunk editing) all work today, and that bugs and rough edges are tracked openly in the issue tracker.

Getting Started

Bad Juju has two pieces:

  1. The server — a Rust LSP binary called badjuju. Every editor integration talks to this same server.
  2. A client — the editor-side glue that launches the server and forwards commands. Each editor has its own setup.

You'll install both. The server first, then the client for whichever editor you use.

Prerequisites

  • Jujutsu (jj) on your PATH. Bad Juju drives jj under the hood; without it, nothing works.

  • Rust (edition 2024 or later) if you're building from source.

  • pnpm 10+ if you plan to build the VS Code extension or run client tests.

  • A build runner. Bad Juju uses apenwarr's redo — see the Getting Started guide for installation instructions (on macOS, brew install redo).

    If you'd rather not install redo, the repo ships a self-contained ./do shell script as a drop-in replacement. Anywhere this guide says redo <target> you can substitute ./do <target> instead.

1. Install the server

There's no published release yet, so install from a checkout:

git clone https://github.com/jennings/badjuju
cd badjuju
redo server/install

This builds the badjuju binary in release mode and installs it to ~/.cargo/bin/badjuju. Make sure ~/.cargo/bin is on your PATH.

Verify the install:

badjuju --version

2. Install a client

Pick the editor you use day-to-day:

  • Neovim — plugin-manager recipes for lazy.nvim, packer, vim-plug, pathogen, Vundle, and built-in pack/ directories.
  • VS Code — install the extension from the marketplace or build a local VSIX.
  • Emacs — recipes for use-package, straight.el, and Doom Emacs.
  • Other editors — Helix is supported via a languages.toml snippet, and any LSP-capable editor can drive Bad Juju through code actions.

3. Open the status window

Once the server is installed and your editor knows how to launch it, open a Jujutsu repository and ask for the status window. Each client has its own entry point — see Basic Usage for the exact commands per editor.

The first time you open the status window in a workspace, Bad Juju creates a .jj/badjuju/ directory for the buffers it writes. There's nothing to clean up later — it lives alongside .jj/ and gets ignored along with the rest of the Jujutsu metadata.

You're ready. Head to Basic Usage for a tour of the everyday workflows.

Usage

This section walks through the workflows you'll use day-to-day.

  • Basic Usage — open the status window, create a new revision, edit a description, view a diff.
  • Manipulating Commits — abandon, reword, squash, and rewrite history from inside your editor.

For an exhaustive reference of what each buffer contains and the keys it responds to, see the Reference chapter.

Basic Usage

This page covers the everyday actions: looking at your working copy, starting a new change, editing a commit message, and viewing a diff. The examples use the default magit keymap that ships with each client — if you've rebound them, substitute your own keys.

Opening the status window

The status window is your home base. It shows what files you've touched, the stack of commits leading up to @ (the working copy), and a one-screen command reference at the bottom.

To open it:

ClientHow
Neovim:JJStatus
VS CodeCommand Palette → jj: Status
EmacsM-x badjuju-status
Helixhx "$(badjuju status)" from the shell

You'll see something like:

STATUS:

The working copy has no changes.
Working copy  (@) : kpkzwvqm 909679d0 (empty) (no description set)
Parent commit (@-): xorwskru 66bfbfdf feat(neovim): buffer-local keymaps...

STACK: ancestors(reachable(@, mutable()), 2)

@  kpkzwvqm 909679d0 1min stephen@example.com
│  (empty) (no description set)
○  xorwskru 66bfbfdf 2min stephen@example.com
│  feat(neovim): buffer-local keymaps on status.jj and log.jj
◆  spxlzwpr 18d66a82 20min stephen@example.com main
│  fix(ci): set DESTDIR when installing redo
~

COMMAND REFERENCE:
n     new change
L     open log
d     describe
...

Once the buffer is open, single-key bindings (or M-x / Command Palette equivalents) drive every other action. Press ? at any time to see the active key map for the current buffer.

Creating a new revision

You finished a commit and want to start working on the next thing? That's jj new, but you don't need to leave the editor:

ClientKey
Neovim, VS Code (magit)n
Emacsn
Helixcode action New child of <rev>

n runs jj new against the working copy. If you'd rather branch off a different commit, place your cursor on that commit's header line first — in clients with hotkeys, the cursor position is what determines the target.

Editing an existing revision

There are two flavors of "edit" in Jujutsu:

  • Move @ to a commit so you can keep editing its working tree. Press e (or run :JJEdit / M-x badjuju-edit) with the cursor on the commit you want to land on. Bad Juju runs jj edit <rev> and refreshes the status window.

  • Change a commit's message without touching its tree. That's describe. Press d with the cursor on the commit; Bad Juju opens a describe.jujutsu buffer pre-populated with the current message. Edit it, save, and Bad Juju calls jj describe -m for you.

In the describe buffer:

ClientFinalizeAbort
Neovim<C-c><C-c><C-c><C-k>
VS CodeCtrl+EnterEscape Escape
EmacsC-c C-cC-c C-k
Helix:write then :quit:quit!

Lines starting with JJ: are comments — they're stripped before the message is saved, so you can leave reminders in them.

Viewing a diff

There are two diff modes:

  • Change diff — pinned to a change id (the stable identifier that follows a commit as you amend it). The diff buffer refreshes automatically when the change is amended. This is what you want most of the time.
  • Commit diff — pinned to an immutable commit id. The view never changes; useful for "what did this exact snapshot look like?"

To open a change diff, place the cursor on a commit and press D. In Emacs, that's D from the status or log buffer; in VS Code/Neovim magit, also D. To open a commit diff in VS Code, use Ctrl+Shift+D.

You can have multiple diff buffers open simultaneously — each one is a separate file (diff-change-<id>.jujutsu or diff-commit-<id>.jujutsu), so you can compare two revisions side by side.

Viewing a single file's history

For the commits that touched one file, place the cursor on a file row in the status buffer and press l f (Magit/Vim profile) or pick Log <file> from the Helix code-action menu. Bad Juju opens a per-file buffer showing jj log -r ..@ -p restricted to that path — see the log file buffer reference.

Refreshing and closing buffers

Bad Juju auto-refreshes open status, log, and diff buffers when a jj operation happens — whether you triggered it through Bad Juju or through jj in a terminal. You should rarely need to refresh manually, but if you want to:

  • Press R (Neovim, VS Code, Emacs) in the buffer.
  • Or run :JJRefresh / M-x badjuju-refresh / the command-palette refresh action.

To close a buffer: press q. In Helix, use :bd (buffer close).

What's next

You've got the basics. Up next:

  • Manipulating Commits covers abandoning, rewording, and squashing — the operations that actually rewrite history.
  • The Reference chapter has the full catalog of each buffer and the keys it responds to.

Manipulating Commits

Once you can navigate the status window, the next step is rewriting history. Jujutsu is built around the assumption that commits are mutable — you reword them, split them, squash hunks between them, and move them around. Bad Juju exposes those operations as cursor-driven actions on the status and log buffers.

This page walks through the most common ones.

Abandoning a change

If you decide a commit shouldn't exist at all — say you started a spike, hated it, and want it gone — abandon it. The commit's descendants get rebased onto its parent automatically.

  1. Open the status or log buffer.
  2. Place the cursor on the commit you want to delete.
  3. Press a (magit profile in Neovim/VS Code/Emacs) or invoke the Abandon commit <rev> code action.

Bad Juju runs jj abandon <rev> and refreshes the buffer. With no cursor on a commit row, a defaults to the working copy.

Heads up: Jujutsu's op log makes this reversible. If you abandoned the wrong thing, press u (or U in Emacs) to invoke jj undo.

Rewording a commit (describe)

If the commit is fine but the message isn't — typo, missing context, wrong issue number — you want to describe it.

  1. Place the cursor on the commit.
  2. Press d.

Bad Juju opens describe.jujutsu populated with the existing message. Lines beginning with JJ: are comments that get stripped on save. Save and close (<C-c><C-c>, Ctrl+Enter, or C-c C-c depending on the editor) to apply.

If you change your mind, abort instead of saving (<C-c><C-k>, Escape Escape, or C-c C-k).

Squashing a single file into the parent

Suppose you realize a change you made belongs in the previous commit, not in your current working copy. Squashing a file moves its changes from @ (the working copy) into the parent commit.

  1. Open the status buffer.
  2. Place the cursor on the file in the WORKING COPY CHANGES list.
  3. Press s.

The file disappears from @'s changes and lands in the parent. If the working copy ends up empty, you can keep working on the same commit or move on with n (new).

Need to pull a file back out of the parent? Press U (or Ctrl+K U in VS Code if U is shadowed by another keymap) — that's unsquash.

Multiple parents? If the working copy is a merge commit, Bad Juju will pick the parent that already touches the file. If the file isn't in either parent (or both), the client prompts you to pick one.

Squashing changes between revisions

Suppose you decide a change should be moved to a different revision — not just the immediate parent, and maybe only some of the hunks. Bad Juju's commit-to-commit squash workflow handles this.

The flow has three steps: mark a source, mark a destination, then pick the hunks.

1. Mark the source

Place your cursor on the commit whose changes you want to move out of. Press s (Emacs) or the Squash from this revision code action (VS Code, Neovim, Helix).

The status/log buffer header updates to confirm the pending source.

2. Mark the destination

Now move the cursor to the commit you want the changes to land in. Press s again (Emacs) or invoke Squash into this revision.

Bad Juju materializes a squash window at .jj/badjuju/squash/<from>-<to>.jujutsu. It has two sections:

  • REMAINING CHANGES — every hunk in the source that hasn't been selected yet.
  • SELECTED CHANGES — the hunks you're moving into the destination.

Initially, everything is in REMAINING.

3. Pick the hunks

In the squash window, navigate to a hunk and toggle it between REMAINING and SELECTED:

ClientKey
Neovim, VS Codes (toggle hunk/file under cursor)
Emacss
Helixcode action Move hunk to SELECTED / Move hunk to REMAINING

You can also:

  • Select everything with a (Emacs) or Move all hunks to SELECTED — equivalent to a plain jj squash.
  • Deselect everything with A (Emacs) or Move all hunks to REMAINING.
  • Edit a hunk before squashing. Press e (Emacs) on a hunk to open hunk-edit.jujutsu. You can tweak the additions and context lines, save, and Bad Juju applies the edited hunk via jj squash --interactive. See the Hunk edit buffer reference for details.

4. Finalize

Close the squash window when you're happy with the SELECTED set. Bad Juju applies the move and refreshes the status/log buffers. If you change your mind partway through, cancel the pending squash via the Cancel pending squash action (or just close the squash window with everything still in REMAINING — nothing gets moved).

Rebasing onto a different destination

To move a commit (and its descendants) onto a new parent, press r with the cursor on the commit you want to rebase. The client prompts for a destination revset; on submit, Bad Juju runs jj rebase -r <src> -d <dest>.

Managing bookmarks

Press b to open the bookmark manager. It's a single popup/picker that handles create, move, delete, track, and forget operations — the same five things jj bookmark does, just from inside your editor.

Undoing the last operation

Jujutsu records every mutating operation in its op log, which makes mistakes cheap. To undo whatever you just did:

ClientKey
Neovim, VS Code (magit)u
EmacsU
Helixcode action via shell jj undo

This invokes jj undo and refreshes the status buffer.

Pulling from / pushing to the Git remote

If your repo is colocated with Git, you can fetch and push without leaving the editor:

ActionKey
jj git fetchf
jj git pushp
jj git push --force-with-leaseP

These are best-effort wrappers — they run the corresponding jj command and surface the output. For anything more nuanced (specific remotes, branches, or refspecs), drop into the terminal.

Clients

Bad Juju ships first-party client integrations for five editors. They all talk to the same badjuju LSP server, so the underlying operations are identical — what differs is the install steps and the exact key bindings.

  • Neovim — Lua plugin with :JJ* commands and buffer-local keymaps. Supports lazy.nvim, packer, vim-plug, pathogen, Vundle, and built-in pack/ directories.
  • VS Code — extension with Command Palette entries and configurable keymap profiles (magit, vim, none).
  • Emacseglot-powered package modeled on Magit, with transient popup menus and M-x badjuju-* commands.
  • Kakoune — kak-lsp plugin with :JJ* commands and user-mode keymaps (magit or vim profile).
  • Other Editors — Helix support via languages.toml plus notes for any LSP-capable editor that can fire code actions.

Neovim

The Neovim client lives in clients/neovim/ in the repo. It uses Neovim 0.11's built-in LSP API (vim.lsp.enable) with automatic workspace detection (root_markers = { '.jj' }).

Requirements

Installation

Pick the recipe that matches your plugin manager. Replace /absolute/path/to/bad-juju with the path to your local checkout.

{
  dir = '/absolute/path/to/bad-juju/clients/neovim',
  name = 'bad-juju',
  ft = 'jujutsu',
  opts = {},
}

packer.nvim

use {
  '/absolute/path/to/bad-juju/clients/neovim',
  config = function() require('badjuju').setup({}) end,
}

vim-plug

Plug 'jennings/bad-juju', { 'rtp': 'clients/neovim' }
require('badjuju').setup({})

pathogen

ln -s /absolute/path/to/bad-juju/clients/neovim \
  ~/.vim/bundle/bad-juju

Vundle

Plugin 'jennings/bad-juju'
set rtp+=~/.vim/bundle/bad-juju/clients/neovim

Neovim built-in packages

ln -s /absolute/path/to/bad-juju/clients/neovim \
  ~/.local/share/nvim/site/pack/badjuju/start/bad-juju

Manual / no plugin manager

vim.opt.rtp:prepend('/absolute/path/to/bad-juju/clients/neovim')
require('badjuju').setup({})

Configuration

setup() is optional. Only call it if you want to override defaults:

require('badjuju').setup({
  -- Path to the jj binary; forwarded to the server.
  binaryPath = nil,
  -- Default revset for :JJLog when called with no argument.
  defaultLogRevset = nil,
  -- Hotkey profile: "magit" (default), "vim", or "none".
  keymapProfile = nil,
})

Commands

CommandDescription
:JJStatusOpen .jj/badjuju/status.jujutsu
:JJLog [revset]Open the log; defaults to @
:JJLogFile [revset]Open the per-file log for the current buffer's file; defaults to ..@
:JJDescribe [revision]Edit a commit message (defaults to @)
:JJDiff [revision]Open a diff (defaults to @)
:JJNewCreate a new change
:JJRefreshRefresh the badjuju buffer at the cursor
:JJSquash [file] [revision]Squash a file into its parent
:JJUnsquash [file] [revision]Unsquash a file from parent into child
:JJUndoRun jj undo and refresh
:JJAbandon [revision]Abandon a revision (defaults to @)

Commands auto-start the LSP for the current workspace if it isn't already running.

Keymaps

Two built-in profiles, plus "none" to disable defaults. Set keymapProfile in setup() to switch.

magit profile (default)

status.jujutsu

KeyAction
RRefresh
nNew change
LOpen log
l fOpen the per-file log for the file at cursor
fGit fetch
pGit push
PGit push --force-with-lease
eEdit commit at cursor (move @)
b c/m/d/t/fBookmark: create / move / delete / track / forget
r sMark rebase source (jj rebase -s)
r rMark rebase source (jj rebase -r)
r bMark rebase source (jj rebase -b)
r oRebase onto commit at cursor (-d)
r AInsert after commit at cursor (--insert-after)
r BInsert before commit at cursor (--insert-before)
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
sSelect squash source or destination (two-step)
SSquash file at cursor into parent
uUnsquash file at cursor
aAbandon commit at cursor
UUndo
xCancel pending operation (squash or rebase)
=Diff (alias for d)
qClose window
?Show key binding help

log.jujutsu

KeyAction
RRefresh
eEdit commit at cursor (move @)
b c/m/d/t/fBookmark: create / move / delete / track / forget
r sMark rebase source (jj rebase -s)
r rMark rebase source (jj rebase -r)
r bMark rebase source (jj rebase -b)
r oRebase onto commit at cursor (-d)
r AInsert after commit at cursor (--insert-after)
r BInsert before commit at cursor (--insert-before)
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
sSelect squash source or destination (two-step)
aAbandon commit at cursor
UUndo
xCancel pending operation (squash or rebase)
=Diff (alias for d)
<CR>Apply revset shortcut on cursor line
qClose window
?Show key binding help

diff.jujutsu

KeyAction
RRefresh (re-runs jj diff)
qClose window
?Show key binding help

describe.jujutsu

Key (mode)Action
<C-c><C-c> (normal)Finalize commit (save and close)
<C-c><C-k> (normal)Abort (close without saving)
<C-c> (insert)Finalize commit (save and close)
? (normal)Show key binding help

vim profile

Two-letter verb chords inspired by Fugitive. Most actions use doubled letters (nn, dd, etc.) to keep single keys free for text navigation:

KeyAction
nnNew change
llOpen log
eeEdit commit at cursor (move @)
ddDescribe commit at cursor
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
=Diff (alias for d)
bb c/m/d/t/fBookmark: create / move / delete / track / forget
rr sMark rebase source (jj rebase -s)
rr rMark rebase source (jj rebase -r)
rr bMark rebase source (jj rebase -b)
rr oRebase onto commit at cursor (-d)
rr AInsert after commit at cursor (--insert-after)
rr BInsert before commit at cursor (--insert-before)
ssSelect squash source or destination (two-step)
SSSquash file at cursor into parent
uuUnsquash file at cursor
UUUndo
aaAbandon commit at cursor
xCancel pending operation (squash or rebase)
ffGit fetch
ppGit push
PPGit push --force-with-lease

Enable with:

require('badjuju').setup({ keymapProfile = 'vim' })

none profile

Disables all built-in keymaps. Define your own using the :JJ* commands above.

Auto-refresh

Open status, log, and diff buffers auto-refresh whenever a jj operation runs — even one you ran in a terminal. No manual reload required.

Syntax highlighting

Highlights come from the LSP via semantic tokens. Your colorscheme's standard token groups (comments, keywords, strings, types, enum members, numbers, operators) are picked up automatically; no extra configuration needed.

VS Code

The VS Code extension lives in clients/vscode/ and is the most feature-complete client today.

Requirements

  • VS Code (any recent version)
  • badjuju server binary on your PATH (see Getting Started)

Installation

From the marketplace

(Coming soon.) For now, build and install a local VSIX.

Build a local VSIX

# From the repo root:
redo clients/vscode/install

This builds the VSIX for your platform and installs it via code --install-extension --force. The code CLI must be on your PATH — in VS Code, run Shell Command: Install 'code' command in PATH from the Command Palette.

For other platforms or a packaged set of VSIXs:

# Single non-host VSIX
TARGET=x86_64-unknown-linux-gnu redo clients/vscode/all

# All platforms at once (requires zig + cargo-zigbuild)
redo clients/vscode/pack

Commands

Open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P) and type jj to filter.

Command IDPalette nameDescription
badjuju.status.openjj: StatusOpen status / change stack
badjuju.log.openjj: Open logOpen the revision log
badjuju.describe.openjj: Describe working copyEdit the current commit message
badjuju.new.openjj: New commitCreate a new empty change
badjuju.next.openjj: Move forward (jj next)Move @ to next child
badjuju.next.editjj: Edit next in placeEdit next change in place
badjuju.prev.openjj: Move back (jj prev)Move @ to previous parent
badjuju.prev.editjj: Edit previous in placeEdit previous change in place
badjuju.refresh.openjj: RefreshRe-run the current buffer's command
badjuju.undo.openjj: Undo last operationUndo last jj op
badjuju.fetch.runjj: Git fetchRun jj git fetch
badjuju.push.normaljj: Git pushRun jj git push
badjuju.push.forceWithLeasejj: Git push --force-with-leaseForce push with lease
badjuju.edit.cursorjj: Edit commit at cursorMove @ to commit under cursor
badjuju.abandon.cursorjj: Abandon commit at cursorAbandon commit under cursor
badjuju.diff.cursorjj: Show diff for commit at cursorDiff for commit under cursor
badjuju.describe.finalizejj: Finalize commit descriptionSave and close describe buffer
badjuju.squash.filejj: Squash file at cursorMove file under cursor into parent
badjuju.unsquash.filejj: Unsquash file at cursorPull file back from parent
badjuju.rebase.source.cursorjj: Mark rebase sourceMark source + mode for two-step rebase
badjuju.rebase.commit.cursorjj: Execute pending rebaseExecute rebase to commit at cursor
badjuju.cancel.runjj: Cancel pending operationCancel pending squash or rebase
badjuju.bookmark.promptjj: BookmarkInteractive bookmark manager
badjuju.log.applyShortcutjj: Apply revset shortcutFollow revset link in log
badjuju.help.openjj: Show hotkey helpCheat sheet for current buffer
badjuju.version.openjj: Show versionServer and jj versions
badjuju.restartLanguageServerjj: Restart Language ServerRestart the LSP

Keymap profiles

Set badjuju.keymapProfile to choose:

  • "magit" (default) — single-key bindings
  • "vim" — two-letter verb chords
  • "none" — no built-in keymaps

magit profile — selected bindings

status.jujutsu / log.jujutsu

KeyAction
RRefresh
nNew commit
LOpen log
Ctrl+N / Ctrl+PMove forward / back (jj next / jj prev)
Ctrl+Shift+N / Ctrl+Shift+PEdit next / previous in place
f / p / PFetch / push / force push
eEdit commit at cursor
bBookmark
r s / r r / r bMark rebase source (--source / --revisions / --branch)
r o / r A / r BExecute rebase (onto / insert-after / insert-before)
dDiff (change)
DDiff (commit, pinned)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
sSelect squash source or destination (two-step)
SSquash file at cursor into parent
xCancel pending operation (squash or rebase)
u, Ctrl+K uUnsquash file at cursor
aAbandon
UUndo
=Diff (alias for d)
qClose
?Help

Squash window (.jj/badjuju/squash/*.jujutsu)

KeyAction
sToggle hunk / file between SELECTED and REMAINING
eEdit hunk before squashing
aSelect all changes
ADeselect all changes
uUndo
TabToggle fold
qClose

describe.jujutsu

KeyAction
Ctrl+EnterFinalize commit (save and close)
Escape EscapeAbort (close without saving)
?Help

vim profile

Doubled letters: nn, ll, dd, ss, SS, uu, UU, aa, ee, bb, ff, pp, PP. Rebase chords use a three-key sequence: r r s / r r r / r r b for source, r r o / r r A / r r B for destination. x cancels any pending operation. Single-key bindings: D, =, q, ?. See the in-repo clients/vscode/README.md for the complete table.

Settings

SettingPurpose
badjuju.binaryPathPath to the jj binary; blank uses PATH
badjuju.defaultLogRevsetDefault revset for badjuju.log.open
badjuju.keymapProfilemagit, vim, or none

Auto-refresh

Open status, log, and diff buffers auto-refresh when jj operations happen — including ones triggered from a terminal outside VS Code.

Emacs

The Emacs client lives in clients/emacs/ and is modeled on Magit. It uses eglot (Emacs 29+ built-in LSP client) and transient (the popup-menu library Magit itself uses).

Requirements

  • Emacs 29+ (ships built-in eglot and transient)
  • badjuju server binary on your PATH
  • jj on your PATH

Installation

Vanilla Emacs (manual)

(add-to-list 'load-path "/path/to/badjuju/clients/emacs")
(require 'badjuju)

use-package

(use-package badjuju
  :load-path "/path/to/badjuju/clients/emacs"
  :commands (badjuju-status badjuju-log badjuju-diff))

straight.el

(straight-use-package
 '(badjuju :type git
            :host github
            :repo "jennings/badjuju"
            :files ("clients/emacs/*.el")))

Or with use-package integration:

(use-package badjuju
  :straight (badjuju :type git
                      :host github
                      :repo "jennings/badjuju"
                      :files ("clients/emacs/*.el"))
  :commands (badjuju-status badjuju-log badjuju-diff))

Doom Emacs

In packages.el:

(package! badjuju
  :recipe (:host github :repo "jennings/badjuju"
           :files ("clients/emacs/*.el")))

In config.el:

(use-package! badjuju
  :commands (badjuju-status badjuju-log badjuju-diff)
  :config
  (map! :leader "g j" #'badjuju-status))

Configuration

;; Path to the jj binary; nil uses PATH.
(setq badjuju-binary-path nil)

;; Hotkey profile: "magit" (default) or "none".
(setq badjuju-keymap-profile "magit")

Top-level commands

CommandDescription
M-x badjuju-statusOpen the working-copy status view
M-x badjuju-logOpen the commit log
M-x badjuju-diffDiff for @ (change mode, updates on amend)
M-x badjuju-describeEdit the commit message for @
M-x badjuju-newCreate a new child change
M-x badjuju-editMove @ to a different commit
M-x badjuju-abandonAbandon the working copy
M-x badjuju-squashSquash into the parent
M-x badjuju-unsquashPull a file back from the parent
M-x badjuju-undoUndo the last operation
M-x badjuju-fetchjj git fetch
M-x badjuju-pushjj git push
M-x badjuju-refreshRefresh the current buffer

Most workflows live in the status buffer — open it with M-x badjuju-status and use the hotkeys below. Press ? at any time for a popup of active bindings in the current buffer.

Keybindings (magit profile)

Status buffer

KeyAction
nNew child change
cCommit transient (reword / new child)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
=Diff (alias for d)
eEdit commit at cursor (move @)
aAbandon commit at cursor
sSelect squash source or destination (two-step)
SSquash file at cursor into parent
uUnsquash file at cursor
UUndo last operation
r sMark rebase source (--source)
r rMark rebase source (--revisions)
r bMark rebase source (--branch)
r oExecute rebase onto cursor
r AExecute rebase insert-after cursor
r BExecute rebase insert-before cursor
xCancel pending squash or rebase
bBookmark manager
fjj git fetch
pjj git push
Pjj git push --force-with-lease
LOpen log
RRefresh
TABToggle fold at cursor
RETGo to definition
gdGo to definition
?Show help popup
qBury buffer

Code actions intentionally have no default binding — use the global M-x eglot-code-actions (Emacs binds it to C-c C-a by default in eglot-managed buffers).

Log buffer

KeyAction
cCommit transient (reword / new child)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
=Diff (alias for d)
eEdit commit at cursor
aAbandon commit at cursor
sSelect squash source or destination (two-step)
SSquash file at cursor into parent
UUndo
r sMark rebase source (--source)
r rMark rebase source (--revisions)
r bMark rebase source (--branch)
r oExecute rebase onto cursor
r AExecute rebase insert-after cursor
r BExecute rebase insert-before cursor
xCancel pending squash or rebase
bBookmark manager
RRefresh
RETApply revset shortcut on a JJ: line / go to definition
gdGo to definition
?Show help popup
qBury buffer

Diff buffer

KeyAction
RRefresh
RET / gdGo to definition
?Show help popup
qBury buffer

Squash buffer

KeyAction
sToggle hunk/file between SELECTED and REMAINING
eEdit hunk before squashing
aSelect all changes
ADeselect all changes
uUndo
TABToggle fold
gdGo to definition
?Show help popup
qBury buffer

Describe buffer

KeyAction
C-c C-cFinalize and close (saves the commit message)
C-c C-kAbort without saving

Folding

Status and squash buffers open fully folded. The WORKING COPY CHANGES and PARENT CHANGES sections are automatically expanded on first open; TAB toggles individual sections.

Kakoune

The Kakoune client lives in clients/kakoune/ in the repo. It uses kak-lsp to talk to the badjuju LSP server and exposes all operations as :JJ* commands with an optional keymap via Kakoune user-modes.

Requirements

  • Kakoune 2023.08.05+
  • kak-lsp 0.14+ on your PATH
  • jj on your PATH
  • badjuju binary on your PATH (see Getting Started)

Install

plug.kak

plug "jennings/bad-juju" subset [clients/kakoune/badjuju.kak] config %{
    # Optional: switch to vim-profile chords (default: magit)
    # set-option global badjuju_keymap_profile vim
}

Manual clone

Symlink the entry point into your autoload directory:

ln -s /absolute/path/to/bad-juju/clients/kakoune/badjuju.kak \
      ~/.config/kak/autoload/badjuju.kak

Or source it directly from your kakrc:

source '/absolute/path/to/bad-juju/clients/kakoune/badjuju.kak'

Setup

1. Configure kak-lsp

Merge the kak-lsp.toml snippet from this directory into ~/.config/kak-lsp/kak-lsp.toml:

cat /path/to/bad-juju/clients/kakoune/kak-lsp.toml \
    >> ~/.config/kak-lsp/kak-lsp.toml

Then start kak-lsp in your kakrc (if not already):

eval %sh{ kak-lsp --kakoune -s $kak_session }

2. Choose a keymap profile

The default is magit (single-letter bindings). To switch to the vim profile (double-letter chords), set the option before sourcing the plugin:

set-option global badjuju_keymap_profile vim
source '/path/to/badjuju.kak'

Or in plug.kak config:

plug "jennings/bad-juju" ... config %{
    set-option global badjuju_keymap_profile vim
}

Opening buffers

The canonical one-liner opens the working-copy status view in Kakoune:

kak "$(badjuju status)"

Similarly for other views:

kak "$(badjuju log)"
kak "$(badjuju diff)"
kak "$(badjuju diff --revision abc123)"

Commands

CommandDescription
:JJStatusOpen .jj/badjuju/status.jujutsu
:JJLog [revset]Open the log buffer
:JJLogFileOpen per-file log for the file at cursor
:JJDescribe [revision]Edit a commit message (default: @)
:JJDiff [revision]Open a change diff (updates on amend)
:JJDiffCommit [revision]Open a pinned commit diff
:JJNewCreate a new change
:JJNextMove @ to the next child
:JJPrevMove @ to the previous parent
:JJRefreshRefresh the current badjuju buffer
:JJSquashSquash file at cursor into its parent
:JJUnsquashUnsquash file at cursor from parent
:JJUndoUndo the last jj operation
:JJAbandon [revision]Abandon a revision (default: @ at cursor)
:JJEdit [revision]Move @ to this revision
:JJFetchRun jj git fetch
:JJPush [!]Run jj git push (! for --force-with-lease)
:JJCancelCancel pending squash or rebase

Commands auto-start the LSP for the current workspace if it isn't already running.

Keymap reference

With a *.jujutsu buffer active, press <space> to enter the badjuju user-mode. The bindings below use the magit profile (default).

magit profile — status.jujutsu

KeyAction
RRefresh
nNew change
LOpen log
fGit fetch
pGit push
PGit push --force-with-lease
UUndo
aAbandon revision at cursor
eEdit commit at cursor (move @)
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned)
sSelect squash source/dest (two-step)
SSquash file at cursor into parent
uUnsquash file at cursor
xCancel pending operation
qClose buffer
bBookmark… (chord)
rRebase… (chord)
cCommit… (chord)
?Show key binding help

magit profile — log.jujutsu

KeyAction
RRefresh
nNew change
UUndo
aAbandon revision at cursor
eEdit commit at cursor (move @)
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned)
sSelect squash source/dest (two-step)
xCancel pending operation
qClose buffer
bBookmark… (chord)
rRebase… (chord)
cCommit… (chord)
?Show key binding help

magit profile — diff.jujutsu

KeyAction
RRefresh
qClose buffer
?Show key binding help

magit profile — squash window

KeyAction
sToggle hunk/file at cursor
eEdit hunk before squashing
aSelect all changes
ADeselect all changes
qClose buffer
?Show key binding help

magit profile — describe.jujutsu

KeyAction
<C-c><C-c>Save and close (finalize commit message)
<C-c><C-k>Abort (close without saving)
?Show key binding help

Chord workflows

Bookmark (b prefix in magit / bb prefix in vim)

KeyAction
cCreate bookmark (prompts for name)
mMove bookmark (prompts for name)
dDelete bookmark (prompts for name)
tTrack remote bookmark (prompts for name@remote)
fForget bookmark (prompts for name)

Rebase (r / rr)

KeyAction
sMark source with --source
rMark source with --revisions
bMark source with --branch
oComplete: rebase onto this commit
AComplete: insert after this commit
BComplete: insert before this commit

Commit transient (c / cc)

KeyAction
wReword — open describe.jujutsu
nNew child commit

Commit-to-commit squash

  1. Press s on the source commit in status or log → server marks it.
  2. Press s on the destination commit → server opens the squash window.
  3. In the squash window, use s/e/a/A to manage hunks.
  4. :w finalizes the squash; :q! aborts.

vim profile

Double-letter chords inspired by Fugitive. Enable with:

set-option global badjuju_keymap_profile vim
KeyAction
nnNew change
llOpen log
eeEdit commit at cursor (move @)
ddDescribe commit
ddDiff change at cursor (updates on amend)
DDDiff commit at cursor (pinned)
ssSquash source/dest (two-step)
SSSquash file at cursor
uuUnsquash file at cursor
UUUndo
aaAbandon revision
ffGit fetch
ppGit push
PPGit push --force-with-lease
RRRefresh
qqClose buffer
x / xxCancel pending operation
bb + c/m/d/t/fBookmark chord
rr + s/r/b/o/A/BRebase chord
cc + w/nCommit transient chord

Save flow

  • describe.jujutsu: edit the commit message, then :w runs jj describe -m <message>. <C-c><C-c> saves and closes; <C-c><C-k> aborts (magit profile).
  • hunk-edit-*.jujutsu: move hunks between SELECTED/REMAINING sections, then :w applies the selection via jj squash.

Customization

Override the <space> leader

Re-bind <space> after sourcing the plugin:

hook global WinSetOption filetype=jujutsu %{
    map window normal <tab> ': enter-user-mode badjuju-status<ret>'
}

Disable the default keymap

Set the profile to any unrecognized value; the source block in badjuju.kak will then source keymap-magit.kak (fallback). To truly disable, edit the sourcing logic or just don't map <space>:

hook global WinSetOption filetype=jujutsu %{
    unmap window normal <space>
}

Troubleshooting

kak-lsp not attaching to jujutsu buffers

Check that the [language.jujutsu] section is present in ~/.config/kak-lsp/kak-lsp.toml and that kak-lsp is running:

:lsp-show-server

:w on describe.jujutsu has no effect

Make sure include_text_on_save = true is set in your kak-lsp.toml snippet. Without it, the server receives the save event but no text.

Auto-reload not firing after mutations

workspace/applyEdit is the mechanism the server uses to push updated content to open buffers. Verify kak-lsp is attached (lsp-show-server) and that it supports apply-edit (kak-lsp 0.14+).

Other Editors

Bad Juju is "just" an LSP server, which means any editor with a sufficiently capable LSP client can drive it. The first-party Helix configuration is documented here as the canonical example; the same principles apply to other LSP-capable editors.

Helix

Helix has no plugin system, so the integration is a small languages.toml snippet plus a few CLI one-liners.

Requirements

  • Helix 25.01+
  • jj and badjuju on your PATH

Setup

  1. Install the server. See Getting Started.
  2. Merge the language config. Copy clients/helix/languages.toml from the repo into either:
    • ~/.config/helix/languages.toml (user-wide), or
    • .helix/languages.toml at the root of your project (per-project).

Opening buffers

Helix doesn't auto-open files returned by code actions, so the entry point is the shell:

hx "$(badjuju status)"
hx "$(badjuju log)"
hx "$(badjuju diff)"                       # change diff for @
hx "$(badjuju diff --revision abc123)"     # diff a specific revision

You can open multiple diffs at once:

hx "$(badjuju diff --revision abc)" "$(badjuju diff --revision def)"

Once a .jujutsu buffer is open, Helix uses Space a for code actions. With the cursor on a commit row you'll get:

ActionDescription
Edit commit <rev>Move @ to this commit
Abandon commit <rev>Delete this commit
Describe commit <rev>Edit commit message
Show diff for <rev>Open the change diff
New child of <rev>jj new <rev>
Bookmark <rev>Bookmark management menu
Squash from this revisionMark this commit as squash source
Rebase --source from this revisionMark source for jj rebase -s
Rebase --revisions from this revisionMark source for jj rebase -r
Rebase --branch from this revisionMark source for jj rebase -b

After marking a squash source, Squash into this revision and Cancel pending operation appear on every commit row.

After marking a rebase source, three destination actions appear:

ActionDescription
Rebase onto this revisionjj rebase … -d <dest>
Insert after this revisionjj rebase … --insert-after <dest>
Insert before this revisionjj rebase … --insert-before <dest>
Cancel pending operationClear the pending rebase source

See Manipulating Commits for the full walkthrough.

For file rows in the status buffer:

ActionDescription
Squash <file>Move file from @ into parent
Unsquash <file>Pull file from parent back into @
Log <file>Open the log file buffer for this path

The Log <file> action opens .jj/badjuju/file/<path>.jujutsu on demand. Helix doesn't auto-open files returned by code actions, so open it manually after invoking the action.

For squash-window rows:

ActionDescription
Move hunk to SELECTED / REMAININGToggle the hunk under the cursor
Move file <name> to SELECTED / REMAININGToggle a whole file
Move all hunks to SELECTED / REMAININGSelect / deselect everything

Log shortcuts

In log.jujutsu, lines beginning with JJ: are revset shortcuts. Place the cursor on one and choose Apply revset: <label> from Space a to re-run the log with that revset.

Other clients bind Enter/RET to a context-aware dispatch — on shortcut lines it applies the revset, elsewhere it falls through to go-to-definition. Helix has no keybinding layer in Bad Juju, so use Space a for both.

Auto-reload

When a jj operation runs outside Helix, the server pushes the refreshed buffer content via workspace/applyEdit. Helix's handler marks the buffer modified after the edit even though the on-disk file already matches; :write is then a no-op. You can ignore the modified indicator until your next real edit.

Known limitations

Helix doesn't auto-open files returned by code actions. If Show diff for <rev> or Squash into this revision report a new file path, open it manually:

:open .jj/badjuju/diff-change-<id>.jujutsu
:open .jj/badjuju/squash/<from>-<to>.jujutsu

Any other LSP-capable editor

If your editor:

  • Speaks the Language Server Protocol,
  • Can launch a stdio LSP server with a custom command, and
  • Can invoke code actions (textDocument/codeAction),

…then it can drive Bad Juju. You'll need to wire up:

  1. A filetype for *.jujutsu files.
  2. An LSP server config that launches badjuju lsp and detects the workspace via a .jj/ marker.
  3. (Optional) Keybindings or commands that send workspace/executeCommand for the badjuju.* operations listed in the Status buffer reference and elsewhere.

Reading the VS Code extension source, the Helix languages.toml snippet, or the Kakoune client (which uses kak-lsp and exposes full command dispatch and keymaps) will give you a working template. If you build an integration for a new editor, please open an issue — we'd love to include it.

Reference

Bad Juju produces a handful of buffer types, each with its own layout and key handlers. This chapter is the detailed tour of each:

  • Status buffer (status.jujutsu) — working copy summary, stack view, command reference.
  • Log buffer (log.jujutsu) — revision log with editable REVSET: header and JJ: shortcut lines.
  • Diff buffer (diff-change-<id>.jujutsu / diff-commit-<id>.jujutsu) — change-mode and commit-mode diffs.
  • Hunk edit buffer (hunk-edit.jujutsu) — interactive squash with line-level edits.

For the everyday-task version of this material, see Usage. For client-specific keybindings, see Clients.

Status buffer

The status buffer lives at .jj/badjuju/status.jujutsu and is your home base. It combines jj status, the working-copy stack (jj log over a focused revset), and a one-screen command reference.

Layout

STATUS:

<jj status output>

STACK: <revset expression>

<jj log output over STACK>

COMMAND REFERENCE:
<one line per available action>

STATUS: section

Shows whether the working copy has changes, and the working-copy commit (@) and its parent (@-). This is whatever jj status prints, verbatim.

STACK: section

The STACK: line is a revset expression — by default ancestors(reachable(@, mutable()), 2), which shows the working copy, every commit you can still rewrite, and two layers of immutable ancestors for context.

Some clients support --stat rendering server-side; when enabled, each commit's line is followed by a per-file change summary.

COMMAND REFERENCE: section

A short, buffer-specific cheat sheet describing the key bindings or commands available in this buffer. The contents vary slightly by client; clients can also override the reference via initialization options. The aim is that you never have to leave the buffer to remember what s does.

Generated commands

The server exposes these LSP commands, which clients map to keys or menu entries:

CommandWhat it does
badjuju.status(Re)write status.jujutsu and return its URI
badjuju.refreshRe-run the command that produced the current buffer
badjuju.newjj new (with optional cursor-target revision)
badjuju.next / badjuju.prevMove @ forward / back
badjuju.editMove @ to the commit at the cursor
badjuju.abandonAbandon the commit at the cursor (or @)
badjuju.describeOpen describe buffer for cursor commit
badjuju.diffOpen change diff for cursor commit
badjuju.diff.commitOpen commit diff for cursor commit
badjuju.squashSquash file at cursor into parent
badjuju.unsquashUnsquash file at cursor from parent
badjuju.squash.commitMark source / open squash window
badjuju.rebase.sourceMark rebase source (--source, --revisions, or --branch)
badjuju.rebase.commitExecute pending rebase (--destination, --insert-after, or --insert-before)
badjuju.cancelClear any pending operation (squash or rebase)
badjuju.undojj undo
badjuju.fetchjj git fetch
badjuju.pushjj git push (with optional forceWithLease)
badjuju.bookmarkInteractive bookmark manager
badjuju.keymap / badjuju.helpShow the active key map
badjuju.versionDisplay badjuju and jj versions

See Clients for the per-editor key bindings that invoke these commands.

Cursor targeting

Cursor-driven actions (edit, abandon, describe, diff, squash, unsquash, rebase.source, rebase.commit, bookmark) read the cursor line and identify a commit or file:

  • On a commit-header row (e.g. the line with @ kpkzwvqm 909679d0 …), the target is that commit's change ID.
  • On a file row in the STATUS: section, the target is that file relative to @.

If the cursor is on something else (a blank line, a section header, the command reference), most actions either operate on @ as a sensible default or report that no target was found.

Auto-refresh

Open status buffers refresh automatically whenever a jj operation runs — whether it came from Bad Juju, a different editor, or a terminal jj invocation. The server watches the op log for new heads and pushes updated content to every open client.

Folding

Some clients (notably Emacs) open the status buffer fully folded by default, with the WORKING COPY CHANGES and PARENT CHANGES sections expanded. Use the editor's fold key (TAB in Emacs) to toggle individual sections.

Log buffer

The log buffer lives at .jj/badjuju/log.jujutsu. It runs jj log against a configurable revset and lets you both edit that revset inline and jump to predefined shortcuts.

Layout

REVSET: <current revset>
JJ: <shortcut label>:  <shortcut revset>
JJ: <shortcut label>:  <shortcut revset>
...

OUTPUT:

<jj log output for REVSET>

COMMAND REFERENCE:
<one line per available action>

REVSET: header

The first line is an editable revset expression. Save the buffer after editing this line and Bad Juju re-runs jj log with the new expression and rewrites the buffer in place.

The default value is whatever revset you opened the log with — @ if you called :JJLog / badjuju-log / palette jj: Open log with no argument, or a configured default (badjuju.defaultLogRevset in VS Code, defaultLogRevset in Neovim setup()).

JJ: shortcut lines

Each JJ: line is a named shortcut: a label and a revset expression. Place the cursor on a shortcut line and:

  • In Neovim, VS Code, Emacs (magit profile): press Enter. The editor invokes badjuju.log.applyShortcut, which replaces REVSET: with the shortcut's revset and re-runs the log.
  • In Helix: press Space a and pick Apply revset: <label>.

Shortcut lines are also useful as documentation — they're plain text in the buffer, so you can copy them, edit them, or paste them into the REVSET: line by hand.

OUTPUT: section

This is the raw output of jj log -r <REVSET>. With --stat enabled (toggle via =), each commit row is followed by its per-file change summary.

Cursor-driven actions

Most of the same actions as the status buffer work in the log: edit, abandon, describe, diff, rebase.source, rebase.commit, cancel, bookmark, squash.commit. The cursor must be on a commit row (one of the lines emitted by jj log).

Enter on a JJ: line is special-cased to apply the shortcut. Some clients (Neovim/VS Code with the magit profile, Emacs) also fall back to go-to-definition on Enter when the cursor isn't on a shortcut line.

Auto-refresh

Like the status buffer, the log auto-refreshes whenever a jj operation runs. The REVSET: value is preserved across refreshes, so editing the header gives you a sticky filtered log.

Log file buffer

The log file buffer shows the history of a single file — every commit that touched the path, each followed by its inline diff. It's the equivalent of:

jj log -r ..@ -p path/to/file.txt

Modeled on Magit's magit-log-buffer-file: a separate buffer per file, distinct from the main log buffer.

Opening the buffer

Place the cursor on a file row inside the status buffer and:

  • Magit profile (Neovim, VS Code, Emacs): press l f.
  • Vim profile: press l f.
  • Helix: press Space a and pick Log <file>.

You can also drive it directly:

  • Neovim: :JJLogFile path/to/file.txt
  • VS Code: Command Palette → jj: Log file…
  • Emacs: M-x badjuju-log-file

Re-invoking the command on a path that's already open reuses the same buffer with refreshed content.

Delivery

Client capabilityDeliveryURI
Virtual (VS Code, Neovim)workspace/textDocumentContentbadjuju-filelog:///<repo-rel-path>
File-based (Helix, Emacs)Physical file.jj/badjuju/file/<repo-rel-path>.jujutsu

The .jujutsu suffix on the physical path keeps the buffer in jujutsu syntax in every editor — matching the status.jujutsu / log.jujutsu / diff-*.jujutsu convention.

Layout

FILE: <repo-relative path>
REVSET: <current revset>
JJ: <shortcut label>:  <shortcut revset>
JJ: <shortcut label>:  <shortcut revset>
...

OUTPUT:

<jj log -p output for REVSET, restricted to FILE>

COMMAND REFERENCE:
<one line per available action>

FILE: header

Workspace-relative path of the file being viewed. Saving the buffer after editing this header (file-based clients only) regenerates the buffer for the new path.

REVSET: header

The default revset is ..@ — every commit reachable from the working copy minus the root. Saving the buffer after editing this header (file-based clients only) reruns the query with the new revset and rewrites the buffer in place.

JJ: shortcut lines

Same as the regular log buffer. Apply via Enter (Magit profile) or Space a (Helix) to substitute the shortcut's revset into the REVSET: header.

OUTPUT: section

The raw output of jj log -r <REVSET> -p -- <FILE>. Each commit's header is followed by the unified diff of that commit's changes to the file.

Per-file buffer model

Each path has its own URI / on-disk file, keyed by the workspace- relative path. Opening the same file twice doesn't create a new buffer; the existing one is refreshed.

The view is per file — there is no per-(file, revset) variant in this release. Changing the REVSET: header replaces the query for that file.

Auto-refresh

Like the status and log buffers, the file-history buffer auto-refreshes after every jj operation. The FILE: and REVSET: headers are preserved across refreshes.

Out of scope

The following are intentionally not supported in this release:

  • --follow for renames.
  • Region-restricted log (Magit's -L).
  • Per-(file, revset) buffers so the same file's history with different revsets can coexist.

Diff buffer

Bad Juju produces diffs in two flavors, distinguished by URI scheme (or filename, on file-based clients):

  • Change diff — pinned to a change ID. Auto-refreshes when the change is amended. This is what badjuju.diff / d opens.
  • Commit diff — pinned to an immutable commit ID. The view is frozen to that exact snapshot. Opened via badjuju.diff.commit (D in VS Code, Neovim, and Emacs).

Filenames and URIs

ModeVirtual URI (VS Code, Neovim)File-based (Helix)
Changebadjuju-diff:///change/<id>.jj/badjuju/diff-change-<12char>.jujutsu
Commitbadjuju-diff:///commit/<id>.jj/badjuju/diff-commit-<12char>.jujutsu

The server detects whether the client supports virtual URIs (via initializationOptions.virtualDiffs: true) and picks the delivery mode accordingly:

  • Virtual-capable clients (VS Code, Neovim) receive diff content through the LSP 3.18 workspace/textDocumentContent request — no file ever hits disk.
  • File-based clients (Helix) get a real file under .jj/badjuju/.

After mutations to a change (describe, new, squash, etc.) the server sends a workspace/textDocumentContent/refresh for every open change-diff URI, or rewrites the on-disk file for file-based clients. Commit diffs are never refreshed.

Layout

<jj diff -r <rev> output>

That's it — the buffer is just whatever jj diff printed. Unlike the status and log buffers, there's no COMMAND REFERENCE: section appended.

Opening multiple diffs

Because each diff is keyed by ID, you can have any number of diff buffers open simultaneously. Use this to compare two revisions:

# Helix example
hx "$(badjuju diff --revision abc)" "$(badjuju diff --revision def)"

In VS Code and Neovim you can open as many diffs as you like via the command palette / :JJDiff — they all live as separate virtual URIs.

Key bindings

The diff buffer has a minimal key map:

KeyAction
RRefresh (re-runs jj diff for the same revision)
qClose window
?Show key binding help

Emacs additionally maps RET / gd to go-to-definition. Code actions intentionally have no badjuju-specific binding in any client — use your editor's native binding (M-x eglot-code-actions in Emacs, vim.lsp.buf.code_action in Neovim, Cmd+. / Ctrl+. in VS Code).

Auto-refresh

Change diffs auto-refresh whenever the change they target is amended — by any operation, including ones triggered outside Bad Juju. Commit diffs never refresh; they're frozen to their commit ID.

Hunk edit buffer

The hunk edit buffer lives at .jj/badjuju/hunk-edit.jujutsu. It's opened from inside a squash window when you want to tweak the contents of a single hunk before squashing it into the destination commit.

When you'd use it

You're in a commit-to-commit squash (see Manipulating Commits). You've picked a hunk to move from the source commit into the destination, but a couple of lines in that hunk don't actually belong in the destination. Rather than splitting the hunk by hand or moving the whole thing and re-editing afterward, you edit it once, at selection time.

How it works

  1. From the squash window, place the cursor on a hunk header.

  2. Press e (Emacs) or invoke the Edit hunk code action.

  3. Bad Juju opens hunk-edit.jujutsu populated with the hunk's contents:

    JJ: Editing hunk in <file>
    JJ: Lines beginning with '-' are deletions and cannot be edited.
    JJ: Edit '+' (added) and ' ' (context) lines; save to apply.
    <file path>
    @@ -<old_start>,<old_len> +<new_start>,<new_len> @@
     context line
    -deleted line
    +added line
     context line
    
  4. Edit the + (additions) and (context) lines. - (deletion) lines are read-only — editing them has no effect.

  5. Save. Bad Juju:

    • Recomputes the @@ header (the line lengths after your edits).
    • Runs jj squash --interactive --tool badjuju to apply the edited hunk to the source commit.
    • Refreshes the squash window so you can continue picking hunks.

Status messages

The buffer's first action after save is to print a terminal status line:

StatusMeaning
EDIT APPLIEDThe hunk was applied successfully.
EDIT ABORTEDThe body was cleared; no change was made.
STALE SOURCEThe source commit was abandoned (or otherwise rewritten) while you were editing. Reopen the squash window and try again.

Constraints

  • Only one hunk-edit buffer at a time. The path is .jj/badjuju/hunk-edit.jujutsu — a single shared location. If you open a hunk-edit for a second hunk, it replaces the first.
  • The buffer is only meaningful when the parent squash window is still alive. Closing the squash window invalidates the edit.
  • JJ:-prefixed lines and the @@ header are advisory metadata. Don't edit them — Bad Juju regenerates them on save based on your edits to the body.

Key bindings

ClientFinalize (save)Close without saving
Neovim:write:bd!
VS CodeCtrl+SClose editor without save
EmacsC-x C-sC-c C-k (where bound)

The buffer behaves like a normal editor file — save semantics drive the apply step, not a dedicated keybinding.