emrg: fix Windows TUI CJK input + legacy arrow keys + /rant project visibility - #546
Conversation
…scan-code fallback) and show evolution-workspace projects in /rant
argszero
left a comment
There was a problem hiding this comment.
❌ Needs fix: InputParser 0xE0 intercept garbles valid UTF-8 (U+0800-U+0FFF scripts) — POSIX regression
Repro (run against 3f51bb0):
InputParser().feed(bytes([0xE0, 0xB8, 0x81])) # Thai 'ก' (U+0E01)
→ [b'\xe0\xb8', b'\x81'] (should be [b'\xe0\xb8\x81'])
InputParser().feed(bytes([0xE0, 0xA4, 0x85])) # Devanagari 'अ' (U+0905)
→ [b'\xe0\xa4', b'\x85'] (should be [b'\xe0\xa4\x85'])
InputParser().feed(b'\x00A') # Ctrl+@ then 'A' (POSIX)
→ [b'\x00A'] (should be [b'\x00', b'A'])
Root cause: the InputParser intercept in feed() treats 0xE0 as a legacy scan-code prefix unconditionally. But 0xE0 is also the UTF-8 lead byte for U+0800-U+0FFF (Devanagari, Bengali, Thai, Tibetan, etc.). _utf8_len(0xE0) correctly waited 3 bytes before this PR; the intercept now consumes 2 bytes prematurely. Same for 0x00: lone NUL (Ctrl+@) followed by any key in the same read gets swallowed.
parse_keypress is already safe (it only returns when the scan code is in the map), so only the InputParser branch needs the gate.
Suggested fix:
if b in (0xE0, 0x00):
if len(self._buf) < 2:
break # wait for the scan-code byte
ansi = normalize_legacy_scan_codes(bytes(self._buf[:2]))
if ansi is not None:
del self._buf[:2]
results.append(ansi)
continue
# Not a recognized scan code — fall through. Valid UTF-8 after 0xE0
# is always 0xA0-0xBF, disjoint from scan codes 0x47-0x53, so this is
# exact. Lone 0x00 also falls through to the single-byte path.Also update test_legacy_unknown_scan_passthrough — its current assertion (feed(b'\xe0\xff') == [b'\xe0\xff']) encodes the buggy behavior; the correct behavior for an invalid 0xE0 pair is to wait for the 3-byte UTF-8 sequence (pre-PR behavior).
Lesson referenced: #455 — verification logic must be validated in both positive AND negative states. The added tests covered recognized scan codes (positive) but not the 0xE0-led UTF-8 case (negative).
…UTF-8 (Thai/Devanagari) must not be consumed as scan pairs
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-190422 (reviewing fix at 723c1f7; angle: regression-resolution verification)
The prior ❌ (4882245397, against 3f51bb0) flagged the InputParser 0xE0 intercept garbling U+0800-U+0FFF UTF-8. This review verifies the fix at the new head resolves it without new regressions.
Fix correctness (723c1f7): the intercept now only consumes the 2-byte pair when normalize_legacy_scan_codes returns a recognized ANSI sequence; otherwise it falls through to normal processing. The disjointness argument holds: scan codes are 0x47-0x53, valid UTF-8 continuation bytes after a 0xE0 lead are 0xA0-0xBF — the ranges never overlap, so gating on the map is exact (no false intercept, no missed scan code).
Verified on 723c1f7 (all probes pass):
- Negative (the regression cases): Thai ก (E0 B8 81) and Devanagari अ (E0 A4 85) now survive intact; Ctrl+@ (0x00) + 'A' yields two sequences; invalid pair 0xE0 0xFF correctly waits as UTF-8 (has_pending).
- Positive: ↑/↓/Del legacy scan codes still normalize to ESC[A / ESC[B / ESC[3~.
- Split-read: lone 0xE0 then 0x48 → ESC[A; lone 0xE0 then UTF-8 continuation (B8 81) → intact 3-byte char. Both arrival orders handled.
- CJK 中 unaffected; parse_keypress legacy mapping intact.
- Full suite: 534 passed; CI green (31172467970).
Scope check: 723c1f7 touches only events.py intercept + test_input_parser.py + doc counts — no unrelated changes. The win32.py VT-input flag and daemon.py filter removal from 3f51bb0 are unchanged and were already verified correct.
Vote: 1/3 for head 723c1f7 (the 3f51bb0 ❌ is resolved by this fix; count restarts on the new head).
… path under EVOLUTION_CWD so a restored filter would fail it
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-191724 (first review of head 387a6d4; angle: regression surface)
The 723c1f7→387a6d4 delta is test-only (tests/test_daemon.py +13/−4), so this review audits the full final state against master for regression surface.
1. events.py (+71) — legacy scan-code path
- Intercept is gated on
normalize_legacy_scan_codesreturning a recognized ANSI sequence; unrecognized pairs fall through. Disjointness holds (scan codes 0x47–0x53 vs UTF-8 continuation bytes 0xA0–0xBF after a 0xE0 lead) — no false intercept, no missed scan code. - Split-read safe: lone 0xE0 waits; a later scan code normalizes, a later UTF-8 continuation completes the 3-byte char (both orders verified in prior cycle 190422).
parse_keypresslegacy branch only returns on a map hit — unknown pairs return None, no crash.
2. win32.py (+25) — VT input flag
_RAW_INPUT_MODEnow includes ENABLE_VIRTUAL_TERMINAL_INPUT; SetConsoleMode failure falls back to window-input-only (pre-Win10-1607). Output mode and binary-mode logic untouched. No change to the restore path (disable_raw_modeuses the saved pre-change mode).
3. daemon.py (+19/−16) — filter removal
_handle_list_projectsreturns all registered projects.yml entries; no filter._touch_projectstill skips evolution subdirs, so evolution cycles' cwd is never auto-tracked. The removed filter was the only behavioral change; error handling (yaml/OSError) preserved.
4. Tests (+126) — discriminating power confirmed
- The evolution-workspace test now registers the emrg path UNDER the monkeypatched EVOLUTION_CWD, so restoring the old filter would exclude it and fail the assertion (verified by simulation: old-filter KEEP=False → excluded → test fails).
- Input tests pin both the fix (Thai/Devanagari/NUL survive) and the feature (scan codes normalize); all fail on the respective regressions.
5. Docs — counts synced to 534 (README/README.cn/Agent.md) per the #511 guard.
Verification on 387a6d4: 534 passed locally; CI green (31173305692); MERGEABLE + CLEAN; 0 conflict markers.
Vote: 1/3 for head 387a6d4 (count restarted after the test-discriminating fix).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-192222 (2nd review of head 387a6d4; angle: Windows behavior reasoning + end-to-end data-flow trace)
Traced the full Windows input chain to confirm the fix actually reaches the parser:
1. Raw-byte delivery path (the critical question): On Windows, app.py uses a daemon-thread stdin reader (_win_stdin_loop) that does blocking os.read(stdin_fd, 4096) and pushes raw chunks into stdin_queue via call_soon_threadsafe — the main loop then feeds them to InputParser.feed(). So legacy scan-code pairs (0xE0 0x48) arrive as raw bytes exactly as the parser intercept expects. No intermediate decode/translation layer strips them. ✓
2. Both console modes converge: With VT input enabled (the main fix) conhost delivers UTF-8 + ANSI arrows directly; in the fallback (pre-Win10-1607, SetConsoleMode rejects the flag) the parser's gated intercept normalizes 0xE0/0x00 scan codes to the same ANSI sequences. Both paths produce identical ESC [ A/B/C/D bytes into handle_key. ✓
3. Downstream consumers get working arrows: _handle_selector_nav and the /-menu autocomplete both match ANSI ESC [ A/B (data[0]==0x1B, data[1]==0x5B) — which is precisely what the normalized scan codes emit. So legacy arrows now drive session/model/project selectors and the / command menu on Windows. ✓
4. No false-flush of a pending 0xE0: the 50ms lone-ESC flush timer only fires when parser._buf == bytearray(b'\x1b'); a pending 0xE0 prefix does NOT match (verified live: condition False, has_pending True), so a half-arrived scan code is never discarded as a stray Escape. ✓
5. projects_list consumer check: the only reader of projects_list is app.py:602 (the /rant project picker) — removing the filter surfaces the evolution-workspace emrg entry there with no other consumer affected. ✓
Verification on 387a6d4: CI green (31173305692); MERGEABLE + CLEAN.
Vote: 2/3 for head 387a6d4.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle 20260807-192803 (3rd review of head 387a6d4; angle: test discriminating-power re-check on final state)
Independently re-verified every new test's discriminating power by simulating each regression against the final code:
Regression simulations (all caught):
- R1 (revert the gate → unconditional 2-byte consume): Thai ก (E0 B8 81) garbles to [e0b8, 81] and Ctrl+@+'A' collapses to [0041] →
test_e0_led_utf8_not_garbled+test_nul_not_swallowed_with_next_keyboth fail ✓ - R2 (remove the intercept entirely): ↑ (E0 48) hits
_utf8_len=3 and waits (never yields ESC[A); ↓ (00 50) yields [00][50] not ESC[B] →test_legacy_up+test_legacy_down_zero_prefixfail ✓ - R3 (restore the old daemon filter): the test fixture's emrg entry now sits under EVOLUTION_CWD, so the restored filter excludes it →
test_list_projects_includes_evolution_workspacefails ✓ (the 190846 false-confidence defect is confirmed fixed)
Positive sanity on repo code (387a6d4): ↑/↓ normalize to ESC[A/B; Thai survives intact; Ctrl+@ yields two sequences; parse_keypress maps legacy ↑ to KeyName.UP. 534 passed; CI green (31173305692).
Process note: my first verification run accidentally imported the installed pre-fix package (~/.emrg/install/source) instead of the repo — the missing normalize_legacy_scan_codes import surfaced it immediately. Re-ran against repo code (cwd on sys.path) to confirm. Recorded as a lesson: always assert the imported module path when validating branch code.
Vote: 3/3 for head 387a6d4 — merge gate satisfied (3 consecutive ✅ from different cycles on this head: 191724 regression-surface, 192222 Windows data-flow, 192803 discriminating-power; the earlier ❌ predates this head and was resolved by 723c1f7).
…ndows TUI input & /rant visibility entries (#547) Co-authored-by: EMRG Evolution <emrg@argszero.dev>
…p tolerance (#552) Version bump 0.2.10 → 0.2.11 across all 6 version sources (pyproject.toml / emrg/__init__.py / gui/package.json / uv.lock / make-installer.sh / build-runtime.sh). Release for Windows verification: - #541 LLM gzip body tolerance - #543 GUI message display fixes (#544 quick-ref) - #545 Windows GCM silent-fail Stage 1 - #546 Windows TUI CJK input + legacy arrow keys + /rant visibility - #548/#549/#550 GitHub auth in GUI (PAT + device flow + banner, Stage 2) - #551 quick-ref All 548 tests green. Co-authored-by: EMRG Evolution <emrg@argszero.dev>
Two host-reported fixes (rants verbatim below)
Rant 1 (2026-08-07T10:38:21 UTC): Windows TUI 无法输入中文 + / 命令菜单上下键失效
Root cause (host-analyzed):
win32.pyraw mode only setENABLE_WINDOW_INPUT(0x0008), missingENABLE_VIRTUAL_TERMINAL_INPUT(0x0200). Without it conhost delivers OEM code-page bytes (GBK on Chinese systems → UTF-8-assuming input chain garbles CJK IME) and arrow keys as legacy0xE0scan codes that the ANSI-only parser + UTF-8-length logic misread.Fixes:
_RAW_INPUT_MODE = ENABLE_WINDOW_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT— console now delivers UTF-8 keystrokes + standard ANSI arrows.SetConsoleModefailure falls back to window-input-only mode (pre-Win10-1607).normalize_legacy_scan_codes()translates0xE0/0x00+ scan-code pairs (↑=0x48, ↓=0x50, ←=0x4B, →=0x4D, Home/End/PgUp/PgDn/Ins/Del) to their ANSI CSI equivalents;InputParser.feed()intercepts the prefix before_utf8_lencan misread 0xE0 as a 3-byte UTF-8 lead;parse_keypress()maps them toKeyEventnames directly.Rant 2 (2026-08-07T10:48:00 UTC): /rant 项目列表过滤 evolution 工作区导致 emrg 项目消失
Root cause (host-analyzed):
_handle_list_projectsfiltered out any path under~/.emrg/evolution/— correct when evolution workspace was internal noise, obsolete since #489/#490/#535 self-heal made~/.emrg/evolution/emrgthe emrg project's only path on packaged installs.Fix: removed the filter entirely — projects.yml only contains explicitly registered entries, so no unregistered-noise case exists.
_touch_projectstill skips evolution subdirs (evolution cycles' cwd never auto-tracked as a user project).Tests (+12 → 532, docs synced per #511 guard)
Acceptance coverage