Skip to content

feat: auto-close diff preview on tool rejection#31

Open
kam-hak wants to merge 1 commit intoCannon07:mainfrom
kam-hak:feat/auto-close-rejection
Open

feat: auto-close diff preview on tool rejection#31
kam-hak wants to merge 1 commit intoCannon07:mainfrom
kam-hak:feat/auto-close-rejection

Conversation

@kam-hak
Copy link
Copy Markdown
Contributor

@kam-hak kam-hak commented Apr 7, 2026

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_result with is_error: true. The PreToolUse hook input includes transcript_path and tool_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 specific tool_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 diff
  • bin/claude-close-diff.sh -- extract transcript fields, stop watcher on acceptance
  • bin/claude-user-prompt-cleanup.sh -- UserPromptSubmit fallback script

Test 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_open shell 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.

@kam-hak kam-hak force-pushed the feat/auto-close-rejection branch from 1814657 to a7444b3 Compare April 7, 2026 15:17
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
@kam-hak kam-hak force-pushed the feat/auto-close-rejection branch from a7444b3 to a6dada9 Compare April 7, 2026 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-close on rejection

1 participant