Skip to content

Fix mode selector cycles mid-prompt - #193

Closed
Rizky92 wants to merge 1 commit into
nbplugins:mainfrom
Rizky92:bugfix/edit-mode-selector-cycling
Closed

Rizky92 wants to merge 1 commit into
nbplugins:mainfrom
Rizky92:bugfix/edit-mode-selector-cycling

Conversation

@Rizky92

@Rizky92 Rizky92 commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

This PR attempt a fix to mode selector. Per claude code v2.1.301, the mode selector would change mid-prompt. This was caused by a visual change to claude code's status bar, unable to recognize the current mode.

Signed-off-by: Muhammad Rizky Rizaldi <myquinn489@gmail.com>
@Rizky92
Rizky92 marked this pull request as ready for review October 16, 2026 06:28

@oleg68 oleg68 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detector change looks good: gating the extra lines on the ⏸/⏵ glyph keeps menu text from being mistaken for the footer, and the negative test covers that. Two requested changes inline (stuck state on unchecked exception, flaky test). Also optional: sendShiftTab() (manual single Shift+Tab) still sets modeSwitchInProgress = false in its finally and can clear the flag while the worker is running; and please check the pom.xml version bump against current main after rebase.

target = editModeSwitchTarget;
}
}
} catch (IOException | InterruptedException ex) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix, the single-worker retargeting is the right design.

One robustness issue: the old finally { modeSwitchInProgress = false; } was removed, and this catch only handles IOException | InterruptedException. If sendOneShiftTab() throws an unchecked exception (e.g. an NPE because connector became null when the session was stopped mid-switch), editModeSwitchTarget stays non-null forever. After that every sendShiftTabsUntilMode call just "retargets" and returns (no worker ever runs again), and modeSwitchInProgress stays true, so the screen→model mode sync in the poll loop is disabled for the rest of the session.

Catching RuntimeException here reuses the existing cleanup block. (I'd avoid moving the cleanup into finally: on a normal exit it could clobber the target of a new worker started right after the loop ended.)

Suggested change
} catch (IOException | InterruptedException ex) {
} catch (IOException | InterruptedException | RuntimeException ex) {

setModeSwitchPollMs(c, 1);

c.onEditModeComboChanged(EditMode.AUTO);
c.onEditModeComboChanged(EditMode.PLAN);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be flaky: it assumes the second request arrives while worker 1 is still running, which only holds because the fake poll interval is 1 ms and the request follows immediately. On a loaded CI machine worker 1 may finish first, then a second thread is started and senders.size() is 2.

Make it deterministic by blocking the first Shift+Tab until the second request has been issued, e.g.:

java.util.concurrent.CountDownLatch secondIssued = new java.util.concurrent.CountDownLatch(1);
// in the overridden sendOneShiftTab():
senders.add(Thread.currentThread());
try { secondIssued.await(2, java.util.concurrent.TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
ccMode.updateAndGet(i -> (i + 1) % cycle.length);
// in the test body:
c.onEditModeComboChanged(EditMode.AUTO);
c.onEditModeComboChanged(EditMode.PLAN);
secondIssued.countDown();

pass


def dump(term, label):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor / optional: claude-launch-tests/test_edit_mode_footer.py is a capture script without assertions. shutil.which('claude') returns None when Claude is not on PATH, which then fails with a confusing error inside subprocess.Popen; consider an explicit sys.exit('claude not found on PATH').

oleg68 added a commit that referenced this pull request Oct 18, 2026
Supersedes #193 (rebased onto current `main`, review comments
addressed). Original work by @Rizky92 — their commit is kept with
authorship.

## Problem
- Claude Code 2.1.x renders `● high · /effort` below the mode footer
line, so the mode was never detected when idle and the combo jumped to
"Ask on Edit".
- A second mode request during a running switch started a second
Shift+Tab worker, and the two cycled endlessly.

## Fix
- `ScreenContentDetector.detectEditMode`: after the bottom-most line,
falls back to the 2 lines above, only if they start with `⏸`/`⏵`. Old
layouts are checked first, with unchanged rules.
- `ClaudeSessionController`: single worker with a retargetable
`editModeSwitchTarget`.

## Review fixes (on top of the original commit)
- Worker also catches `RuntimeException`, so the in-progress flag is
never stuck (new test).
- Retarget test made deterministic with a `CountDownLatch`.
- `sendShiftTab()` no longer clears `modeSwitchInProgress` while a
worker owns it.
- Capture script exits with a clear message if `claude` is not on PATH.

`mvn package`: 1140 tests, 0 failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Muhammad Rizky Rizaldi <myquinn489@gmail.com>
Co-authored-by: Muhammad Rizky Rizaldi <myquinn489@gmail.com>
Co-authored-by: Oleg Samarin <osamarin@openintegration.inc>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@oleg68

oleg68 commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Replaced with #195

@oleg68 oleg68 closed this Oct 18, 2026
@oleg68 oleg68 reopened this Oct 18, 2026
@oleg68 oleg68 closed this Oct 18, 2026
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.

2 participants