[Bug] 2.1.218 left-arrow guard: obeying the "Press ← again" hint promptly is silently ignored
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
2.1.218 added a guard so the left arrow can't discard a conversation with no undo. When it triggers it shows the hint "Press ← again". Following that instruction promptly does nothing, silently, so the back navigation appears broken.
The cause is the ordering of two checks in the decision function, plus the fact that arming stamps both timestamps. Symbol names below are the minified ones from the 2.1.218 build.
var Dzs=3000, ndp=1000, _3y=150;
function idp(e, t /*now*/, r /*soloKeypress*/, n /*flag*/, o /*attach*/, i /*sessionStart*/){
if(r !== true) return "reject";
let s = (l) => l !== 0 && l >= i;
if(o){ /* attach path */ }
if(!n) return "fire";
if(s(e.lastLeftPressMs) && t - e.lastLeftPressMs < ndp) return "absorb"; // 1000ms, checked FIRST
if(s(e.armedAtMs) && t - e.armedAtMs <= 3000) return "fire";
return s(e.editedEmptyAtMs) && t - e.editedEmptyAtMs < 2000 ? "arm" : "fire";
}
function sdp(e,t,r){
switch(t){
case "fire": e.armedAtMs=0, e.attachConfirmArmedAtMs=0, e.lastLeftPressMs=r; return;
case "arm": e.armedAtMs=r, e.lastLeftPressMs=r; return; // sets BOTH
case "absorb": e.lastLeftPressMs=r; return;
}
}
arm sets lastLeftPressMs as well as armedAtMs. Because the absorb rule is evaluated before the armed rule, the confirming press lands in absorb whenever it arrives within 1000ms, and case "absorb": return U; returns with no feedback at all. The hint itself has a 3000ms timeout (Dzs), so the only window where the confirmation actually works is roughly 1s to 3s after the first press. Pressing sooner, which is what the hint invites, silently re-absorbs and refreshes lastLeftPressMs each time.
The clock that starts this is the non-empty to empty transition:
if(U.text !== "" && Ke.text === "") j.editedEmptyAtMs = Date.now();
Call site, for context:
case "left":
if(Me.superKey) return U.startOfLine();
if(Me.ctrl||Me.meta||Me.fn) return U.prevWord();
if(i && !Me.shift && U.text === ""){ // only when the buffer is ALREADY empty
let Ke=Date.now();
let Ye=idp(j, Ke, Me.soloKeypress, Je("tengu_left_arrow_editing_guard", !0));
switch(sdp(j,Ye,Ke), Ye){
case "fire": return G(J), i(), U;
case "arm": return $({key:J,kind:"feedback",text:s??"Press ← again",priority:"immediate",timeoutMs:Dzs}),
M("tengu_left_arrow_blocked",{reason:Se("editing-quiet")}), U;
case "absorb": return U;
Worth separating from the timing bug: the guard only runs when U.text === "", so at the moment it fires there is no edited text left to protect. The stated goal is avoiding a discard with no undo, but an empty buffer has nothing to discard. Arming on editedEmptyAtMs alone looks like the wrong condition, independent of the ordering issue.
What Should Happen?
After the "Press ← again" hint appears, the next left-arrow press should background the session and open agent view, regardless of how quickly it arrives. A confirmation the user cannot satisfy by doing exactly what it says is worse than no confirmation.
If the debounce is deliberate, the arm state should at minimum not be reachable when the input buffer is empty, and an absorbed press should give some feedback rather than returning silently.
Steps to Reproduce
- Start a session and leave the prompt empty.
- Type any text into the prompt.
- Delete it all the way back to empty (backspace/Ctrl+U).
- Immediately press
←. Nothing happens; the hint "Press ← again" appears. - Press
←again right away, within 1 second, as instructed. Nothing happens, with no feedback. - Keep pressing quickly; it keeps failing, because each absorbed press refreshes
lastLeftPressMs.
Reliable workarounds: wait roughly 1 second after the hint and press ← once, or wait about 2 seconds after emptying the input before pressing ← at all (then editedEmptyAtMs is stale and it fires on the first press).
Suggested Fix
Check the armed state before the absorb window, or exclude an armed press from the absorb debounce:
if(s(e.armedAtMs) && t - e.armedAtMs <= 3000) return "fire";
if(s(e.lastLeftPressMs) && t - e.lastLeftPressMs < ndp) return "absorb";
Separately, consider not arming when the buffer is empty.
Error Messages/Logs
No error output. The blocked presses are reported through telemetry as tengu_left_arrow_blocked with reason editing-quiet, so the frequency of this should already be measurable.
Is this a regression?
Yes. editedEmptyAtMs and the tengu_left_arrow_editing_guard flag are both absent from the 2.1.216 and 2.1.217 bundles and present in 2.1.218. The flag defaults to true, so this may be a staged rollout rather than fully enabled everywhere.
Last Working Version
2.1.217
Version
2.1.218
Environment
macOS (darwin), Apple Terminal.app, TERM=xterm-256color, no tmux. Reproduces in both foreground and background/agents sessions.
Related: #80386 requests documentation for this same 2.1.218 left-arrow change. This report is about the confirmation being unsatisfiable rather than undocumented.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗