diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e115dd5..3865d906 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixed the plugin's Auto Mode indicator incorrectly turning on when a permission prompt (e.g. for a Bash command) merely mentioned "auto mode" in its tip text or "switch to auto mode" menu option, instead of the mode actually changing
- Stopped showing Claude Code's "auto mode classifier billing" session notice when using an OpenAI-compatible or ChatGPT Subscription connection type — classifier requests are billed the same as before, just without the interruption, since this connection type was never able to hand those checks off to the server in the first place
# 1.3.8 (2026-07-22)
diff --git a/pom.xml b/pom.xml
index 1ac3cd4c..4d50c1e7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbplugins
netbeans-plugin-claude-code-gui
- 1.3.30-SNAPSHOT
+ 1.3.31-SNAPSHOT
nbm
Netbeans Plugin Claude Code GUI
NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java b/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java
index 1c3c7806..057b7a09 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java
@@ -12,13 +12,15 @@
*
Constants are ordered from least to most permissive, so
* ordinal comparisons ({@code >=}, {@code <=}) express permission thresholds naturally.
*
- *
Screen-text mapping (used by {@code ScreenContentDetector.detectEditMode}):
+ *
Screen-text mapping (used by {@code ScreenContentDetector.detectEditMode}), checked
+ * against the single bottom-most non-blank screen line only:
*
* - {@link #PLAN} — bottom line contains {@code "plan mode"} or {@code "plan-mode"}
- * - {@link #DEFAULT} — bottom line starts with {@code " esc to interrupt"}
- * (two leading spaces)
+ * - {@link #DEFAULT} — bottom line contains {@code "manual mode on"}, or (fallback for
+ * Claude Code versions that do not emit that text) starts with
+ * {@code " esc to interrupt"} (two leading spaces)
* - {@link #ACCEPT_EDITS} — bottom line contains {@code "accept edits"}
- * - {@link #AUTO} — bottom line contains {@code "auto mode"};
+ *
- {@link #AUTO} — bottom line contains {@code "auto mode on"};
* available on Max/Team/Enterprise/API plans (Claude Code 2.1.83+)
* - {@link #BYPASS_PERMISSIONS} — bottom line contains {@code "bypass permissions"};
* appears when Claude was launched with {@code --dangerously-skip-permissions}
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java b/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java
index 372858e1..658a8ca0 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java
@@ -459,6 +459,10 @@ private static boolean isBoxDrawingLine(String trimmed) {
/**
* Detect the Claude CLI edit mode from the rendered screen footer.
*
+ * Only the single bottom-most non-blank line is inspected — text further up the
+ * screen (e.g. a permission-prompt tip or menu option that merely mentions a mode by
+ * name) must never be mistaken for the actual mode-footer line.
+ *
*
Possible return values:
*
* - {@link EditMode#PLAN} — bottom line contains {@code "plan mode"} or
@@ -468,9 +472,12 @@ private static boolean isBoxDrawingLine(String trimmed) {
*
- {@link EditMode#BYPASS_PERMISSIONS} — bottom line contains
* {@code "bypass permissions"}; present when Claude was launched with
* {@code --dangerously-skip-permissions}
- * - {@link EditMode#DEFAULT} — bottom line starts with
+ *
- {@link EditMode#AUTO} — bottom line contains {@code "auto mode on"}
+ * - {@link EditMode#DEFAULT} — bottom line contains {@code "manual mode on"}, or
+ * (fallback for Claude Code versions that do not emit that text) starts with
* {@code " esc to interrupt"} (two leading spaces)
- * - {@link Optional#empty()} — screen is blank or transitioning
+ * - {@link Optional#empty()} — screen is blank, transitioning, or the bottom line is
+ * not a recognized mode indicator
*
*
* @param lines rendered screen lines
@@ -480,22 +487,24 @@ public Optional detectEditMode(List lines) {
if (lines == null || lines.isEmpty()) return Optional.empty();
List bottom = bottomNonBlankLines(lines, 3);
if (bottom.isEmpty()) return Optional.empty();
- for (String line : bottom) {
- String lower = line.toLowerCase();
- if (lower.contains("plan mode") || lower.contains("plan-mode")) {
- return Optional.of(EditMode.PLAN);
- }
- if (lower.contains("accept edits")) {
- return Optional.of(EditMode.ACCEPT_EDITS);
- }
- if (lower.contains("bypass permissions")) {
- return Optional.of(EditMode.BYPASS_PERMISSIONS);
- }
- if (lower.contains("auto mode")) {
- return Optional.of(EditMode.AUTO);
- }
+ String lower = bottom.get(0).toLowerCase();
+ if (lower.contains("plan mode") || lower.contains("plan-mode")) {
+ return Optional.of(EditMode.PLAN);
+ }
+ if (lower.contains("accept edits")) {
+ return Optional.of(EditMode.ACCEPT_EDITS);
+ }
+ if (lower.contains("bypass permissions")) {
+ return Optional.of(EditMode.BYPASS_PERMISSIONS);
+ }
+ if (lower.contains("auto mode on")) {
+ return Optional.of(EditMode.AUTO);
+ }
+ if (lower.contains("manual mode on")) {
+ return Optional.of(EditMode.DEFAULT);
}
- // " esc to interrupt" with two leading spaces is the reliable Ask/default-mode signal.
+ // " esc to interrupt" with two leading spaces is the reliable Ask/default-mode signal,
+ // kept as a fallback for older Claude Code versions that never emit "manual mode on".
// Without leading spaces the screen is in an unknown / transitioning state.
if (bottom.get(0).startsWith(" esc to interrupt")) {
return Optional.of(EditMode.DEFAULT);
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java b/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java
index 91fb7525..d637ef3c 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java
@@ -240,6 +240,51 @@ void detectEditModeEmptyWhenNullOrEmpty() {
assertTrue(detector.detectEditMode(List.of()).isEmpty());
}
+ @org.junit.jupiter.api.Test
+ void detectEditModeNotAutoWhenAutoModeOnlyMentionedInPermissionPromptTip() {
+ // Regression: a Bash-command permission prompt whose tip/menu text merely *mentions*
+ // "auto mode" must NOT be misdetected as EditMode.AUTO — only the actual bottom-most
+ // footer line indicates the current mode.
+ List lines = new ArrayList<>(List.of(
+ " Bash command",
+ " Tip: auto mode handles these prompts for you — choose \"switch to auto mode\" below",
+ "",
+ " │ cd /home/oleg/my-projects/grandorgue/GrandOrgue && mkdir -p build/perf-baseline-debug",
+ " Configure a scratch Debug build with testing enabled",
+ "",
+ " Do you want to proceed?",
+ " ❯ 1. Yes",
+ " 2. Yes, and switch to auto mode · auto mode handles these prompts for you",
+ " 3. No",
+ "",
+ " Esc to cancel · Tab to amend"
+ ));
+ java.util.Optional result = detector.detectEditMode(lines);
+ assertTrue(result.isEmpty(),
+ "Expected empty Optional — permission-prompt tip/menu text merely mentions "
+ + "\"auto mode\", it is not the mode-footer line");
+ }
+
+ @org.junit.jupiter.api.Test
+ void detectEditModeManualMode() {
+ List lines = new ArrayList<>(List.of(
+ "Some output",
+ "⏵⏵ manual mode on (shift+tab to cycle)"
+ ));
+ for (int i = 0; i < 10; i++) lines.add("");
+ java.util.Optional result = detector.detectEditMode(lines);
+ assertTrue(result.isPresent());
+ assertEquals(EditMode.DEFAULT, result.get());
+ }
+
+ @org.junit.jupiter.api.Test
+ void detectEditModeManualModeCaseInsensitive() {
+ List lines = List.of("Some output", "Manual Mode on | esc to interrupt");
+ java.util.Optional result = detector.detectEditMode(lines);
+ assertTrue(result.isPresent());
+ assertEquals(EditMode.DEFAULT, result.get());
+ }
+
// -------------------------------------------------------------------------
// detectPlanName
// -------------------------------------------------------------------------