diff --git a/.claude/skills/demo-gifs/SKILL.md b/.claude/skills/demo-gifs/SKILL.md index 7af0706..8780416 100644 --- a/.claude/skills/demo-gifs/SKILL.md +++ b/.claude/skills/demo-gifs/SKILL.md @@ -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` | @@ -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 diff --git a/README.md b/README.md index 57f0432..788ff44 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 @@ -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/.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 diff --git a/demo/commit.gif b/demo/commit.gif new file mode 100644 index 0000000..8ffd7b2 Binary files /dev/null and b/demo/commit.gif differ diff --git a/demo/commit.tape b/demo/commit.tape new file mode 100644 index 0000000..76231a7 --- /dev/null +++ b/demo/commit.tape @@ -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 diff --git a/demo/discard.gif b/demo/discard.gif new file mode 100644 index 0000000..2f92219 Binary files /dev/null and b/demo/discard.gif differ diff --git a/demo/discard.tape b/demo/discard.tape new file mode 100644 index 0000000..a1ff892 --- /dev/null +++ b/demo/discard.tape @@ -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 diff --git a/demo/fixture.ts b/demo/fixture.ts index e8e577c..1dc81cc 100644 --- a/demo/fixture.ts +++ b/demo/fixture.ts @@ -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); diff --git a/demo/hero.gif b/demo/hero.gif index 7e6f013..6d33f6d 100644 Binary files a/demo/hero.gif and b/demo/hero.gif differ diff --git a/demo/hero.tape b/demo/hero.tape index 7150cca..0be771e 100644 --- a/demo/hero.tape +++ b/demo/hero.tape @@ -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 diff --git a/demo/into.gif b/demo/into.gif new file mode 100644 index 0000000..2c4ce0b Binary files /dev/null and b/demo/into.gif differ diff --git a/demo/into.tape b/demo/into.tape new file mode 100644 index 0000000..4b7026c --- /dev/null +++ b/demo/into.tape @@ -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 diff --git a/demo/record.sh b/demo/record.sh index a00716e..b6ce646 100755 --- a/demo/record.sh +++ b/demo/record.sh @@ -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