From bfc35148ae58607a43ef20b35935948be9b3c903 Mon Sep 17 00:00:00 2001 From: Amit Singh Date: Fri, 12 Jun 2026 17:59:37 +0530 Subject: [PATCH 1/4] fix(zsh): pad _forge_reset to avoid zle clearing output --- crates/forge_main/src/zsh/plugin.rs | 20 +++++++++++++++----- shell-plugin/lib/dispatcher.zsh | 3 --- shell-plugin/lib/helpers.zsh | 11 +++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index c91ee70a9a..281a7b0de9 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -417,11 +417,21 @@ mod tests { use pretty_assertions::assert_eq; let fixture = generate_zsh_plugin().unwrap(); - let actual = fixture.contains( - " _forge_osc133_emit \"B\"\n _forge_osc133_emit \"C\"\n case \"$user_action\" in", - ) && fixture.contains( - " local action_status=$?\n _forge_osc133_emit \"D;$action_status\"\n _forge_osc133_emit \"A\"\n _forge_reset", - ); + + // Verify OSC 133 B and C markers are emitted before the action dispatch + let actual = fixture.contains(" _forge_osc133_emit \"B\"\n _forge_osc133_emit \"C\"") + // The buffer is redisplayed with cursor at end so user input stays visible + && fixture.contains("CURSOR=${#BUFFER}\n zle redisplay") + // The case dispatch follows the buffer redisplay + && fixture.contains(" case \"$user_action\" in") + // _forge_reset pads with newlines before clearing to prevent ZLE from clearing + // conversation output lines (see helpers.zsh:139-157) + && fixture.contains("for ((_i=1; _i<=pad; _i++)); do echo; done\n") + && fixture.contains("BUFFER=\"\"\n CURSOR=0") + // D and A markers follow the action, with _forge_reset at the end + && fixture.contains( + " local action_status=$?\n _forge_osc133_emit \"D;$action_status\"\n _forge_osc133_emit \"A\"\n _forge_reset", + ); let expected = true; assert_eq!(actual, expected); } diff --git a/shell-plugin/lib/dispatcher.zsh b/shell-plugin/lib/dispatcher.zsh index c5f13db0e0..0be98eb808 100644 --- a/shell-plugin/lib/dispatcher.zsh +++ b/shell-plugin/lib/dispatcher.zsh @@ -118,9 +118,6 @@ function forge-accept-line() { # Add the original command to history before transformation print -s -- "$original_buffer" - # CRITICAL: Move cursor to end so output doesn't overwrite - # Don't clear BUFFER yet - let _forge_reset do that after action completes - # This keeps buffer state consistent if Ctrl+C is pressed CURSOR=${#BUFFER} zle redisplay diff --git a/shell-plugin/lib/helpers.zsh b/shell-plugin/lib/helpers.zsh index 2f038381c6..e95af038d4 100644 --- a/shell-plugin/lib/helpers.zsh +++ b/shell-plugin/lib/helpers.zsh @@ -137,6 +137,17 @@ function _forge_select_model_pair_global() { } function _forge_reset() { + # Print newlines equal to the current buffer display line count so that + # ZLE's zrefresh() clears these padding lines instead of conversation + # output. When BUFFER spans multiple display lines (newlines or terminal + # wrap), ZLE tracks the multi-line state internally (olnct). On + # reset-prompt, zrefresh() compares olnct (old multi-line count) against + # nlnct (new count after clearing) and clears the delta lines. By + # printing padding here, the cleared lines are blank ones we inserted + # rather than the forge conversation output that precedes them. + local pad="${BUFFERLINES:-1}" + for ((_i=1; _i Date: Fri, 12 Jun 2026 18:05:07 +0530 Subject: [PATCH 2/4] fix(zsh): adjust _forge_reset padding loop condition in tests --- crates/forge_main/src/zsh/plugin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index 281a7b0de9..d3fa06a15a 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -426,7 +426,7 @@ mod tests { && fixture.contains(" case \"$user_action\" in") // _forge_reset pads with newlines before clearing to prevent ZLE from clearing // conversation output lines (see helpers.zsh:139-157) - && fixture.contains("for ((_i=1; _i<=pad; _i++)); do echo; done\n") + && fixture.contains("for ((_i=1; _i Date: Fri, 12 Jun 2026 18:11:21 +0530 Subject: [PATCH 3/4] fix(zsh): use print in _forge_reset padding loop --- crates/forge_main/src/zsh/plugin.rs | 2 +- shell-plugin/lib/helpers.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge_main/src/zsh/plugin.rs b/crates/forge_main/src/zsh/plugin.rs index d3fa06a15a..0350f9ad07 100644 --- a/crates/forge_main/src/zsh/plugin.rs +++ b/crates/forge_main/src/zsh/plugin.rs @@ -426,7 +426,7 @@ mod tests { && fixture.contains(" case \"$user_action\" in") // _forge_reset pads with newlines before clearing to prevent ZLE from clearing // conversation output lines (see helpers.zsh:139-157) - && fixture.contains("for ((_i=1; _i Date: Fri, 12 Jun 2026 18:23:38 +0530 Subject: [PATCH 4/4] fix(zsh): pad _forge_reset loop variables to avoid zle clearing --- shell-plugin/lib/helpers.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell-plugin/lib/helpers.zsh b/shell-plugin/lib/helpers.zsh index 8e471ff994..32d2e3f092 100644 --- a/shell-plugin/lib/helpers.zsh +++ b/shell-plugin/lib/helpers.zsh @@ -145,7 +145,7 @@ function _forge_reset() { # nlnct (new count after clearing) and clears the delta lines. By # printing padding here, the cleared lines are blank ones we inserted # rather than the forge conversation output that precedes them. - local pad="${BUFFERLINES:-1}" + local pad="${BUFFERLINES:-1}" _i for ((_i=1; _i