feat: auto-close diff preview on tool rejection#31
Open
kam-hak wants to merge 1 commit intoCannon07:mainfrom
Open
feat: auto-close diff preview on tool rejection#31kam-hak wants to merge 1 commit intoCannon07:mainfrom
kam-hak wants to merge 1 commit intoCannon07:mainfrom
Conversation
1814657 to
a7444b3
Compare
When the user rejects an Edit/Write/MultiEdit at Claude Code's permission prompt, the diff preview tab now closes automatically. No CC hook fires after manual rejection (empirically confirmed: abortController.abort() kills the turn dead). However, the rejection IS written to the session transcript JSONL as a tool_result with is_error:true. The PreToolUse hook input includes transcript_path and tool_use_id, which uniquely identify the pending tool call. Primary mechanism: PreToolUse spawns a background watcher that tail -F's the session transcript JSONL, matching the specific tool_use_id + is_error:true. On match, closes the diff via nvim RPC. The watcher is scoped to the specific session transcript, so rejections from other CC sessions do not affect unrelated diffs. Self-terminates on: acceptance (stopfile from PostToolUse), rejection match, diff manually closed, nvim death, or 120s timeout. Fallback: UserPromptSubmit hook checks is_open() and closes any orphaned diff when the user sends their next message. Includes 24 headless integration tests covering rejection detection, watcher lifecycle, multi-session isolation, rapid accept races, unfocused tab handling, tail death recovery, and state cleanup. All tests report wall-clock timing for critical paths. Closes Cannon07#29
a7444b3 to
a6dada9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #29
Problem
When the user rejects an Edit/Write/MultiEdit at Claude Code's permission prompt, the diff preview tab stays open in neovim. There is no CC hook that fires after manual rejection --
abortController.abort()kills the turn dead (empirically confirmed: no Stop, no PostToolUseFailure, no PermissionDenied, nothing).Approach
There is no true hook for rejection, but there is a consistent side effect: the rejection is written to the session transcript JSONL as a
tool_resultwithis_error: true. The PreToolUse hook input includestranscript_pathandtool_use_id, which uniquely identify the pending tool call.Primary: transcript file watcher. PreToolUse spawns a background process that
tail -Fs the session transcript, matching the specifictool_use_id+is_error: true. On match, closes the diff via nvim RPC (~150ms latency). The watcher is scoped to the specific session transcript that made the edit -- rejections from other CC sessions do not affect unrelated diffs.Fallback: UserPromptSubmit hook. Checks
is_open()and closes any orphaned diff when the user sends their next message. Belt-and-suspenders for cases where the watcher died or the socket changed.Watcher self-terminates on: acceptance (stopfile from PostToolUse), rejection match, diff manually closed, nvim unreachable, or 120s timeout.
Changes
bin/claude-preview-transcript-watch.sh-- watcher library (sourced by both hooks)bin/claude-preview-diff.sh-- extract transcript fields, spawn watcher after showing diffbin/claude-close-diff.sh-- extract transcript fields, stop watcher on acceptancebin/claude-user-prompt-cleanup.sh-- UserPromptSubmit fallback scriptTest plan
24 headless integration tests across 3 layers, all with wall-clock timing:
Layer 1 -- Pure bash (no nvim): rejection line detection (positive, wrong id, acceptance, unrelated, garbage), state dir lifecycle, stopfile,
is_openshell function (live/dead/empty socket)Layer 2 -- Headless nvim integration: diff tab lifecycle, scratch buffer verification, watcher closes diff on rejection, watcher stops on acceptance, watcher ignores wrong tool_use_id
Layer 3 -- Edge cases: second diff replaces first watcher, unfocused tab rejection, rapid accept (stopfile race), multi-session isolation, manual close, tail death recovery, permission bypass (empty fields), state cleanup
Measured latency: rejection to diff close ~150ms e2e.