Skip to content

fix(zsh): prevent output truncation by using send-break for prompt reset#2567

Open
ssddOnTop wants to merge 4 commits intomainfrom
fix/line-truncation
Open

fix(zsh): prevent output truncation by using send-break for prompt reset#2567
ssddOnTop wants to merge 4 commits intomainfrom
fix/line-truncation

Conversation

@ssddOnTop
Copy link
Collaborator

@ssddOnTop ssddOnTop commented Mar 14, 2026

Summary

Fix output truncation in the shell plugin where the last few lines of forge's interactive output were overwritten by the new prompt after command completion.

Context

When a user runs an interactive forge command (e.g., : hello world), the forge binary writes output directly to /dev/tty to bypass ZLE's terminal ownership. After completion, _forge_reset used zle -I + zle reset-prompt to redraw the prompt. However, zle reset-prompt redraws at the cursor position ZLE internally tracks -- which is stale because ZLE has no awareness that the forge binary moved the terminal cursor by writing output to /dev/tty. This caused ZLE to redraw the prompt at the original pre-output row, overwriting the last few lines of output.

Changes

  • Replaced zle -I + zle reset-prompt with zle .send-break in _forge_reset()
  • .send-break (the builtin Ctrl+G handler) aborts the current ZLE edit cycle and triggers a fresh prompt cycle at the actual terminal cursor position, instead of ZLE's stale tracked position

Key Implementation Details

The core issue is a mismatch between ZLE's internal cursor tracking and the real terminal cursor:

  1. forge-accept-line calls zle redisplay, which anchors ZLE's tracked position at row R
  2. _forge_exec_interactive runs the forge binary with </dev/tty >/dev/tty, writing output that moves the real cursor to row R+N
  3. ZLE still believes the cursor is at row R

Why zle reset-prompt fails: It redraws at row R (stale), overwriting the last N lines of output.

Why zle .send-break works: It exits the ZLE edit cycle entirely. ZSH's normal prompt machinery (precmd -> prompt display) then draws the new prompt wherever the terminal cursor actually is (row R+N).

The . prefix calls the builtin send-break, bypassing any user-defined widget of the same name (and our custom forge-accept-line).

Affected actions (those using _forge_exec_interactive which writes to /dev/tty):

  • _forge_action_default -- main AI chat
  • _forge_action_new -- new conversation with prompt
  • _forge_action_login -- provider login

Unaffected actions (those using _forge_exec which goes through ZLE pipes): All other actions (:info, :env, :tools, :config, etc.) -- ZLE's cursor tracking remains accurate for these, and .send-break works correctly as a universal replacement.

Testing

  1. Open a terminal (not tmux -- the bug is most visible in direct terminal emulators)
  2. Run : write me a mass of numbered lines from 1 to 50, each saying "this is line N"
  3. After completion, verify the Finished <uuid> line and all 50 numbered lines are visible (scroll up if needed)
  4. Run : again to test a second conversation on the same session
  5. Verify no lines are overwritten in either case

Links

  • History of _forge_reset iterations: commits a5d77d2c4, ca0fac8bf

@github-actions github-actions bot added the type: fix Iterations on existing features or infrastructure. label Mar 14, 2026
@ssddOnTop ssddOnTop changed the title fix(zsh): use .send-break to avoid stale cursor redraw on reset fix(zsh): prevent output truncation by using send-break for prompt reset Mar 14, 2026
zle -I
zle reset-prompt

zle .accept-line
Copy link
Contributor

Choose a reason for hiding this comment

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

Implementation does not match the PR description. The code calls zle .accept-line but the description explicitly states the fix should use zle .send-break. These are fundamentally different ZLE widgets:

  • .accept-line (Enter key) accepts and executes the current line
  • .send-break (Ctrl+G) aborts the current edit cycle and starts fresh

The description's rationale is that .send-break "aborts the current ZLE edit cycle and triggers a fresh prompt cycle at the actual terminal cursor pos", which is the intended behavior. Using .accept-line instead will execute the empty buffer (after BUFFER="") rather than aborting the edit cycle, which may not properly reset the prompt at the correct cursor position.

Fix:

zle .send-break
Suggested change
zle .accept-line
zle .send-break

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: fix Iterations on existing features or infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants