fix(agent): erase double-width characters cleanly at the prompt - #14
Merged
Conversation
Backspacing a CJK character at the You> prompt left half its glyph on screen. input() was leaving the erase to the tty line discipline, which emits one BS-space-BS per character regardless of how many columns that character occupies -- two, for East Asian wide and fullwidth ones. The buffer really was empty; only the display was stale, so the leftover could not be erased by pressing backspace again. Import readline, which redraws the line from an absolute column instead of counting on the erase being one column wide. Arrow-key editing and in-session history come along with it. The blank line separating turns moves out of the prompt string, since readline measures the prompt to place the cursor and a newline inside it throws that off. The regression test drives the agent over a pty and replays its output onto a model of a terminal line, so it asserts on what the user sees rather than on the escape sequences a given readline build emits.
The repository had no workflow that runs the test suite -- release.yml only fires on a v* tag -- so a pull request reported no checks at all and nothing verified a change before it landed. Run pytest on every pull request and on pushes to main, across the lowest supported Python (3.13) and the newest it runs on (3.14). uv run --frozen installs exactly what uv.lock pins and fails if the lock has drifted from pyproject.toml. The job holds contents:read and nothing more.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits: the fix, and the CI workflow that verifies it.
fix(agent): erase double-width characters cleanly at the prompt
The bug
Backspacing a CJK character at the
You>prompt leaves half of its glyph onscreen, and pressing backspace again does not clear it — the input buffer is
already empty, so there is nothing left to erase:
Why
input()only routes through a line editor when thereadlinemodule has beenimported. Without it, the echo and the erase fall to the tty line discipline,
which emits one
BS space BSper character — clearing exactly one column. EastAsian wide and fullwidth characters occupy two, so half the glyph survives.
A pty capture of the old behaviour shows both halves of the symptom at once:
The fix
src/nanopycodeagent/agent.pyimportsreadline, which redraws the line froman absolute column instead of assuming the erase is one column wide:
The import is guarded — a platform without
readlinekeeps the old behaviourrather than failing to start.
Two things follow from it:
inserted a raw
^[[Ainto the line.readlinemeasures the prompt to place the cursor and a newline inside itthrows that off. The rendered layout is unchanged.
Tests
tests/test_line_editing.pydrives the real agent over a pty and replays itsoutput onto a small model of a terminal line, so it asserts on what the user
sees rather than on the escape sequences a particular
readlinebuild emits(libedit and GNU readline redraw differently, both correctly).
Verified to fail on the unfixed code, reproducing the reported symptom exactly:
The tests skip where they could not mean anything: no
fork(non-POSIX), or anon-UTF-8 ctype such as an explicit
LC_ALL=C, under which multibyte editingcannot work in the first place.
tests/test_agent.pyadds two cheap in-process guards — thatreadlineisimported, and that the prompt carries no embedded newline — so a future edit
cannot quietly undo either half of the fix.
tests/helpers.pynow returns therecorded
input()prompts to make the second one possible.ci: run pytest on pull requests
The repository had no workflow that runs the tests —
release.ymlonly fires ona
v*tag — so pull requests reported no checks at all and nothing verified achange before it landed.
.github/workflows/ci.ymlruns pytest on every pull request and on pushes tomain, across Python 3.13 (the lowest supported) and 3.14 (the newest it runson), with
fail-fast: falseso one version failing does not hide the other'sresult.
uv run --frozeninstalls exactly whatuv.lockpins and fails if thelock has drifted from
pyproject.toml. The job holdscontents: readandnothing more.
This is the first PR whose tests are verified by CI: 54 passed on both versions,
with the pty tests confirmed to have run rather than skipped.
Changelog
A
Fixedentry under[Unreleased]indocs/changelogs/0.5.x.mdcovers theprompt behaviour. The CI workflow gets no entry — it changes nothing for users
of the published package.