Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .claude/skills/demo-gifs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ well enough in prose, and a GIF per command rots faster than it teaches.
| GIF | Shows | Backend | README home |
| --- | --- | --- | --- |
| `hero.gif` | Mark two hunks with `x`, `C`, type a summary, review reloads without them | git | under the opening paragraph, before `## Requirements` |
| `commit.gif` | `C` end to end, including the refusal when something is already staged | git | end of `### Committing` |
| `commit.gif` | `B` end to end: the summary dialog, then the description dialog only `B` asks for | git | end of `### Committing` |
| `into.gif` | `F`, the picker, the squash landing immediately | **jj** | end of `### Putting hunks into an existing commit` |
| `discard.gif` | `D`, the confirmation naming what is lost, the hunks reverting | git | end of `### Discarding` |

Expand All @@ -38,8 +38,14 @@ must be visible inside the frame** — the fixture sets a shell prompt naming it
(`git-demo ~/cart>` / `jj-demo ~/cart>`) so no viewer mistakes jj's immediate
squash for git's deferred one.

No GIFs for `S` (staging is the familiar `git add -p` shape) or `B` (it is `C`
plus one question).
No GIF for `S` — staging is the familiar `git add -p` shape.

No GIF for the refusals either. One was tried: a tape that staged a file and
pressed `C` records as a file going into the index and then *nothing visibly
happening*, because a refusal is a single line of status text. On screen that
reads as a broken demo rather than a guard rail working. Refusals stay in
prose, and `### Committing` gets `B` instead — the one command whose dialogs
nothing else shows.

## Pipeline layout

Expand Down
40 changes: 28 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ In git it refuses, before asking anything, when something is already staged
half-finished. If a `pre-commit` hook rejects the commit, the marked hunks are
unstaged again, so the repository is exactly as it was.

![Pressing B on a marked hunk: a dialog asking for the summary, a second
dialog asking for an optional longer description, then the hunk committed and
gone from the review.](demo/commit.gif)

### Putting hunks into an existing commit

`F` lists the commits you can still change and puts the marked hunks into the
Expand All @@ -106,6 +110,10 @@ What happens next differs, and the confirmation says which:
command the message gives you, at a moment you choose. `--autostash` is part
of it because the hunks you did not mark are still in your working tree.

![In a Jujutsu workspace, pressing F on a marked hunk: a picker of the commits
that can still be changed, a confirmation naming the revision and the jj undo
that reverses it, then the squash landing immediately.](demo/into.gif)

### Discarding

`D` is the opposite of `S`: instead of moving the marked hunks somewhere, it
Expand All @@ -129,6 +137,10 @@ binary file is refused rather than guessed at, because the patch carries no
record of what it held before — revert those with `jj restore` or
`git restore`.

![Marking one hunk and pressing D in a git repository: a confirmation naming
what is about to be lost, then the hunk reverting in the working copy while
the unmarked changes stay.](demo/discard.gif)

## How it works

The marking half is shared. The applying half is not, because the two systems
Expand Down Expand Up @@ -210,20 +222,24 @@ The integration tests build real git and jj repositories in temp directories
and drive the real binaries end to end; each suite skips itself when its binary
is missing.

Layout:
### Recording the demo GIFs

The GIFs above are recordings of the real extension, not mockups. Regenerate
one after changing a key, a prompt, or a confirmation:

```bash
./demo/record.sh hero # or: commit, into, discard
```

| Path | Holds |
| ------------------ | ----------------------------------------------------------- |
| `index.ts` | composition root: Hunk commands, events, highlights |
| `src/patch/` | parsing patches, rebuilding files, writing a patch back out |
| `src/staging/` | the backend port, and what each file contributes |
| `src/git/` | the index backend |
| `src/jj/` | the revision backend, and jj's diff-editor protocol |
| `src/ui/` | marks, painted highlights, wording, settings |
| `src/workspace.ts` | which system a review sits in — jj wins a colocated tie |
It needs `vhs`, `ttyd`, and `ffmpeg` alongside the usual toolchain. Each run
builds a throwaway repository (`demo/fixture.ts`), installs this extension into
a config directory of its own — so no other extension, theme, or update notice
of yours reaches the frame — then records `demo/<name>.tape` and checks the
result: a size budget, a length budget, and a last frame that differs from the
first, which is what catches a recording of a TUI that never took a keystroke.

`src/` never imports from `index.ts`, nothing under `src/patch/` touches the
filesystem, and neither backend knows the other exists.
GIFs are binary, so a Hunk review cannot show you what changed. Watch the file
before committing it.

## License

Expand Down
Binary file added demo/commit.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions demo/commit.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Committing demo: B commits the marked hunks with a description as well as a
# summary. Hunk's input dialog holds one line, so a body costs a second
# question — and only the key that promises one asks it.
#
# Run via ./demo/record.sh commit.

Output demo/commit.gif

Set Shell bash
Set FontSize 16
Set Width 880
Set Height 640
Set Padding 10
Set Framerate 12
Set TypingSpeed 30ms

Hide
Type `export PS1="git-demo ~/cart> " XDG_CONFIG_HOME=/tmp/hunk-commit-demo/config HUNK_DISABLE_UPDATE_NOTICE=1` Enter
Type "cd /tmp/hunk-commit-demo/cart && clear" Enter
Show

Sleep 600ms
Type "hunk diff --theme github-dark" Enter

Sleep 2200ms
Hide
Type "?"
Sleep 800ms
Escape
Sleep 600ms
Show

Sleep 600ms
Type "]"
Sleep 700ms
Type "x"
Sleep 1200ms

# First question: the summary.
Type "B"
Sleep 1800ms
Type "fix: round the taxed total"
Sleep 600ms
Enter

# Second question, which only B asks: the description.
Sleep 1800ms
Type "Totals were carrying full float precision."
Sleep 600ms
Enter

Sleep 3s
Binary file added demo/discard.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions demo/discard.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Discard demo: mark the money fix, press D, and watch it revert in the
# working copy — the opposite of staging, run on the marked hunks.
#
# Run via ./demo/record.sh discard.

Output demo/discard.gif

Set Shell bash
Set FontSize 16
Set Width 880
Set Height 640
Set Padding 10
Set Framerate 12
Set TypingSpeed 30ms

Hide
Type `export PS1="git-demo ~/cart> " XDG_CONFIG_HOME=/tmp/hunk-commit-demo/config HUNK_DISABLE_UPDATE_NOTICE=1` Enter
Type "cd /tmp/hunk-commit-demo/cart && clear" Enter
Show

Sleep 600ms
Type "hunk diff --theme github-dark" Enter

Sleep 2500ms
Hide
Type "?"
Sleep 800ms
Escape
Sleep 600ms
Show

Sleep 600ms
Type "]"
Sleep 700ms
Type "x"
Sleep 1200ms

# D names what is about to be lost, and in git it is not recoverable — the
# confirmation is the whole point of the frame.
Type "D"
Sleep 2500ms
Enter
Sleep 3s
17 changes: 16 additions & 1 deletion demo/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,28 @@ await git("commit", "-q", "-m", "feat: add the cart");
if (backend === "jj") {
// Colocated, so the recording shows jj driving a repository git also
// understands — and `JJ_CONFIG` keeps the machine's real config out of it.
// That isolation has a cost: an empty config makes jj render every revision
// as "(no email set)", which lands in `F`'s commit picker, so the identity
// has to be written back in. It is written beside the repository, not in it:
// jj tracks the working copy, so a config in the root becomes a third
// changed file in the demo.
const jjConfig = join(root, "..", "jjconfig.toml");
await writeFile(
jjConfig,
`[user]
name = "${IDENTITY.GIT_AUTHOR_NAME}"
email = "${IDENTITY.GIT_AUTHOR_EMAIL}"
`,
"utf8",
);
await run("jj", ["git", "init", "--colocate"], {
cwd: root,
env: { ...env, JJ_CONFIG: join(root, ".jjconfig.toml") },
env: { ...env, JJ_CONFIG: jjConfig },
});
}

// The working-copy state the demo opens on.
await writeAll(root, EDITED);


console.log(root);
Binary file modified demo/hero.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion demo/hero.tape
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Set TypingSpeed 30ms
# about where it is running, and `F` behaves differently in git and jj.
# XDG_CONFIG_HOME must match the one record.sh installed the extension into.
Hide
Type `export PS1="git-demo ~/cart> " XDG_CONFIG_HOME=/tmp/hunk-commit-demo/config` Enter
Type `export PS1="git-demo ~/cart> " XDG_CONFIG_HOME=/tmp/hunk-commit-demo/config HUNK_DISABLE_UPDATE_NOTICE=1` Enter
Type "cd /tmp/hunk-commit-demo/cart && clear" Enter
Show

Expand Down
Binary file added demo/into.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions demo/into.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Into demo (jj): mark the money fix, press F, pick the commit it belongs to,
# and watch `jj squash` land it immediately.
#
# jj rather than git because jj's squash completes on screen; git's path ends
# on "now go run this rebase command", which is a dead final frame.
#
# Run via ./demo/record.sh into.

Output demo/into.gif

Set Shell bash
Set FontSize 16
Set Width 880
Set Height 640
Set Padding 10
Set Framerate 12
Set TypingSpeed 30ms

Hide
Type `export PS1="jj-demo ~/cart> " XDG_CONFIG_HOME=/tmp/hunk-commit-demo/config HUNK_DISABLE_UPDATE_NOTICE=1` Enter
Type `export JJ_CONFIG=/tmp/hunk-commit-demo/jjconfig.toml` Enter
Type "cd /tmp/hunk-commit-demo/cart && clear" Enter
Show

Sleep 600ms
Type "hunk diff --theme github-dark" Enter

Sleep 2200ms
Hide
Type "?"
Sleep 800ms
Escape
Sleep 600ms
Show

Sleep 600ms
Type "]"
Sleep 700ms
Type "x"
Sleep 1200ms

# F offers only the commits that can still be changed.
Type "F"
Sleep 2200ms
Enter

# Then it confirms, naming the revision and the `jj undo` that reverses it.
Sleep 2500ms
Enter

# In jj the squash happens now: the hunk is gone from the working copy.
Sleep 3s
12 changes: 10 additions & 2 deletions demo/record.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@ done

tape="${1:-hero}"
backend=git
[ "$tape" = "into" ] && backend=jj
extra=()
case "$tape" in
into) backend=jj ;;
esac

bun "$repo/demo/fixture.ts" --backend "$backend" --out "$work/cart" >/dev/null
bun "$repo/demo/fixture.ts" --backend "$backend" --out "$work/cart" \
"${extra[@]+"${extra[@]}"}" >/dev/null

# The jj tapes read this; the git ones ignore it. Exported here so the isolated
# identity the fixture wrote is the one on screen in `F`'s commit picker.
export JJ_CONFIG="$work/jjconfig.toml"

# An isolated config is what keeps takes identical: no other extension of the
# recording machine's is loaded (one of them collides with `X`), no update
Expand Down
Loading