fix(engine): ⌫ xoá dấu cách mở lại từ vừa chốt - issue #40 - #42
Merged
Conversation
Gõ `thasy` → `tháy`, space, ⌫ xoá dấu cách, rồi gõ `a` để bù dấu: ra `tháya` thay vì `thấy`. Space chốt từ và reset engine sạch; ⌫ với buffer rỗng chỉ resetContext rồi thả phím qua, không có đường nào dựng lại buffer nên `a` là ký tự mới. reEditWord không cứu được: nó chỉ bắt phím dấu THUẦN (s/f/r/x/j/z/w), `a` là chữ cái, và mặc định còn OFF. Mọi bộ gõ khác (iOS/macOS, UniKey, EVKey, OpenKey) đều khôi phục buffer khi ⌫ lùi vào từ đã chốt. ENGINE — snapshot re-open, buffer cố định, zero-alloc: commitBoundary/commitText nhớ đúng chuỗi phím vừa chốt, CHỈ khi màn hình đang hiển thị `composed`: không auto-restore về raw (`google` → screen là "google", buffer sẽ là "gôgle" ⇒ desync), không overflow (view 32 ký tự là prefix cũ của màn hình). reopenLastCommit() replay lại chuỗi đó qua feed() nên đặt thanh, lan ươ, ánh xạ ⌫ và quyết định boundary hành xử y như từ chưa từng bị chốt — không có code path thứ hai phải giữ đồng bộ (khác seed(), vốn dựng lại từ TEXT). Replay được VERIFY như round-trip của seed(): settings đổi giữa commit và ⌫ mà từ không compose ra đúng như cũ thì reset + trả nil, không bao giờ compose đè lên text không tái tạo được. previousWordEnglish cũng khôi phục về giá trị TRƯỚC commit — từ mở lại phải nhìn ngữ cảnh của từ đứng trước nó, không phải ngữ cảnh chính nó vừa sinh ra. Snapshot hẹp có chủ đích: sống qua đúng MỘT lần commit, chết ngay khi phím đầu của từ mới vào feed(), khi reset() (mất focus, click chuột, đổi app), hoặc khi caller gọi forgetLastCommit(). reset() tách làm hai — resetWord() cho hai đường commit (chúng vừa chụp snapshot xong), reset() công khai xoá cả hai. CONTROLLERS — cùng một luật ở cả IMKit lẫn tap (máy người báo lỗi đi tap): 1. Chỉ mở lại khi phím kết thúc từ để lại ĐÚNG 1 ký tự ASCII in được (space/dấu câu/số). Return/Tab/Esc/mũi tên/phím chức năng/dead key → forgetLastCommit(): Enter gửi tin nhắn trong app chat, Tab đổi focus, Esc không chèn gì ⇒ ⌫ sau đó không xoá ký tự ranh giới. commitComposition (app force-commit giữa từ) cũng vậy. 2. Trước khi dùng, ĐỌC LẠI text trên màn hình đúng vị trí từ đó và so khớp — IMKit qua attributedSubstring, tap qua AX. Lệch (app nuốt phím ranh giới, autocorrect/text-substitution đổi từ, caret nhảy, field NFD, AX trễ) → bỏ qua, ⌫ hành xử y như trước. Đây là khác biệt cốt lõi với reEditWord vốn ĐOÁN từ trên màn hình rồi seed (regression 1.4.28 → default OFF): ở đây engine biết chính xác từ nào, đọc màn hình chỉ để xác nhận. 3. Loại trừ per-FIELD giống re-edit: omnibox/search bar (inline autocomplete tự viết đè) và app marked-text. Tap thêm điều kiện emitMode == .backspace. Kèm theo: ⌘/⌃/⌥ và nhánh imeActive trong tap trước bị gate `!engine.isEmpty` nên snapshot sống sót qua ⌘A + ⌫ (⌫ đó xoá SELECTION, không phải ký tự ranh giới) — nay test thêm canReopenLastCommit, vẫn là một phép so Int nên máy đang dùng ABC không trả phí gì mỗi phím. TelexCore 233 test pass (debug + release), AppTests 167 pass. Zero-allocation invariant giữ nguyên, 0.1545 µs/phím (ceiling 0.40).
ptrinh
added a commit
that referenced
this pull request
Aug 13, 2026
Quyết định maintainer 13/08: PR #42 merge always-on, nhưng cả re-edit lẫn ⌫ re-open đều là hành vi "với ngược vào từ trước con trỏ" — một opt-in gác cả đôi, default OFF (bài học 1.4.28). Guard 1 dòng ở cả hai entry point (IMKit + tap, đặt trước cả phép so Int canReopen để máy không opt-in không trả thêm phí); description toggle cập nhật cả ba nơi (code + en + vi): "Xóa dấu cách mở lại từ vừa chốt". Test pin default-OFF của gate (giờ gác hai tính năng). Đồng thời bỏ 3 commit local chưa push của bản seed-from-screen cũ (backup tại branch backup/my-backspace-restore-40) — PR #42 thay thế hoàn toàn với kiến trúc tốt hơn (engine nhớ đúng keystroke + verify màn hình từng ký tự). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ptrinh
added a commit
that referenced
this pull request
Aug 13, 2026
…hật ký (PR #43) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
xkhanhs
pushed a commit
to xkhanhs/vtx
that referenced
this pull request
Aug 13, 2026
… hiện có Quyết định maintainer 13/08: PR ptrinh#42 merge always-on, nhưng cả re-edit lẫn ⌫ re-open đều là hành vi "với ngược vào từ trước con trỏ" — một opt-in gác cả đôi, default OFF (bài học 1.4.28). Guard 1 dòng ở cả hai entry point (IMKit + tap, đặt trước cả phép so Int canReopen để máy không opt-in không trả thêm phí); description toggle cập nhật cả ba nơi (code + en + vi): "Xóa dấu cách mở lại từ vừa chốt". Test pin default-OFF của gate (giờ gác hai tính năng). Đồng thời bỏ 3 commit local chưa push của bản seed-from-screen cũ (backup tại branch backup/my-backspace-restore-40) — PR ptrinh#42 thay thế hoàn toàn với kiến trúc tốt hơn (engine nhớ đúng keystroke + verify màn hình từng ký tự). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ptrinh
added a commit
that referenced
this pull request
Aug 14, 2026
Maintainer 14/08 sau một ngày field-test (Discord/iTerm/Chrome/TextEdit). Doc ghi rõ lịch sử: 03/08 bật → 04/08 revert cùng ngày vì lớp lỗi "seed ĐOÁN từ trên màn hình rồi transform bậy"; PR #42 đã thay đoán bằng snapshot chuỗi phím + đối chiếu màn hình từng ký tự, gate keystream #38 chặn seed xuyên boundary, poke AX (1.5.11) cho Electron — đường gây regression cũ không còn. Test pin cả hai chiều (default ON + explicit OFF vẫn dính, là escape hatch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ptrinh
pushed a commit
that referenced
this pull request
Aug 15, 2026
Trước đây cập nhật chỉ nói "Có bản mới: 1.6.3" — người dùng phải cài trong khi
không biết bản mới có gì. Release body đã nằm sẵn trong response của
/releases/latest nhưng bị bỏ đi, chỉ lấy tag_name và html_url.
Kênh manual lấy changelog miễn phí: body có sẵn trong response vừa parse, không
thêm request nào. Kênh weekly (stable) thì stable.json chỉ có version, nên phải
hỏi thêm /releases/tags/v{version} — đổi lại stable.json vẫn giữ hai dòng
maintainer sửa tay, không phải nhớ cập nhật changelog lúc promote. Notes của
kênh weekly lưu vào pendingUpdateNotes cạnh pendingUpdateVersion (ghi/xoá cùng
nhau) để sống sót qua lần IME khởi động lại giữa lúc check và lúc mở tab.
Fetch notes là best-effort: mọi lỗi trả nil chứ không throw, vì thiếu changelog
không bao giờ được biến một bản update thật thành .failed — số phiên bản là tin
chính, changelog là phần thêm. Có test pin 404.
Body đến từ mạng nên không tin nó ngắn hay đúng định dạng: lọc git trailer
(Co-Authored-By, Signed-off-by — plumbing của commit, không phải tin cho người
dùng), gộp các dòng trống thừa, cap 4000 ký tự. Quá cap thì link "Xem đầy đủ
thay đổi" dẫn ra trang release, URL suy ra từ version nên kênh weekly cũng có
link mà không phải lưu thêm key.
Render Markdown tối giản: Text chỉ hiểu inline markdown, nên tách ## và - thành
block riêng rồi giao phần inline cho AttributedString — không thêm dependency.
Heading theo đúng luật ATX (phải có dấu cách sau #), nếu không thì dòng mở đầu
bằng "#42 sửa lỗi" sẽ bị ăn mất # và in đậm.
Cửa sổ Settings mặc định cao 560 → 660: tab Giới thiệu không có scroll container
và phần cố định của nó đã chiếm gần hết chiều cao, nên hộp changelog là thành
phần duy nhất co được và sẽ bị ép còn một vệt. 660 chỉ áp dụng lần tạo đầu, ai
đã chỉnh kích thước vẫn giữ nguyên.
Blocks parse một lần lúc gán vào @State thay vì parse trong ForEach: body chạy
lại mỗi lần model publish hoặc checking/installing/status đổi.
193 test pass. Layout chưa kiểm tra trực quan — mới đúng về mặt code.
xkhanhs
pushed a commit
to xkhanhs/vtx
that referenced
this pull request
Aug 17, 2026
Trước đây cập nhật chỉ nói "Có bản mới: 1.6.3" — người dùng phải cài trong khi
không biết bản mới có gì. Release body đã nằm sẵn trong response của
/releases/latest nhưng bị bỏ đi, chỉ lấy tag_name và html_url.
Kênh manual lấy changelog miễn phí: body có sẵn trong response vừa parse, không
thêm request nào. Kênh weekly (stable) thì stable.json chỉ có version, nên phải
hỏi thêm /releases/tags/v{version} — đổi lại stable.json vẫn giữ hai dòng
maintainer sửa tay, không phải nhớ cập nhật changelog lúc promote. Notes của
kênh weekly lưu vào pendingUpdateNotes cạnh pendingUpdateVersion (ghi/xoá cùng
nhau) để sống sót qua lần IME khởi động lại giữa lúc check và lúc mở tab.
Fetch notes là best-effort: mọi lỗi trả nil chứ không throw, vì thiếu changelog
không bao giờ được biến một bản update thật thành .failed — số phiên bản là tin
chính, changelog là phần thêm. Có test pin 404.
Body đến từ mạng nên không tin nó ngắn hay đúng định dạng: lọc git trailer
(Co-Authored-By, Signed-off-by — plumbing của commit, không phải tin cho người
dùng), gộp các dòng trống thừa, cap 4000 ký tự. Quá cap thì link "Xem đầy đủ
thay đổi" dẫn ra trang release, URL suy ra từ version nên kênh weekly cũng có
link mà không phải lưu thêm key.
Render Markdown tối giản: Text chỉ hiểu inline markdown, nên tách ## và - thành
block riêng rồi giao phần inline cho AttributedString — không thêm dependency.
Heading theo đúng luật ATX (phải có dấu cách sau #), nếu không thì dòng mở đầu
bằng "ptrinh#42 sửa lỗi" sẽ bị ăn mất # và in đậm.
Cửa sổ Settings mặc định cao 560 → 660: tab Giới thiệu không có scroll container
và phần cố định của nó đã chiếm gần hết chiều cao, nên hộp changelog là thành
phần duy nhất co được và sẽ bị ép còn một vệt. 660 chỉ áp dụng lần tạo đầu, ai
đã chỉnh kích thước vẫn giữ nguyên.
Blocks parse một lần lúc gán vào @State thay vì parse trong ForEach: body chạy
lại mỗi lần model publish hoặc checking/installing/status đổi.
193 test pass. Layout chưa kiểm tra trực quan — mới đúng về mặt code.
(cherry picked from commit f8ca1d1)
xkhanhs
pushed a commit
to xkhanhs/vtx
that referenced
this pull request
Aug 17, 2026
Maintainer 14/08 sau một ngày field-test (Discord/iTerm/Chrome/TextEdit). Doc ghi rõ lịch sử: 03/08 bật → 04/08 revert cùng ngày vì lớp lỗi "seed ĐOÁN từ trên màn hình rồi transform bậy"; PR ptrinh#42 đã thay đoán bằng snapshot chuỗi phím + đối chiếu màn hình từng ký tự, gate keystream ptrinh#38 chặn seed xuyên boundary, poke AX (1.5.11) cho Electron — đường gây regression cũ không còn. Test pin cả hai chiều (default ON + explicit OFF vẫn dính, là escape hatch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit f3a7ccd)
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.
Gõ
thasy→tháy, space, ⌫ xoá dấu cách, rồi gõađể bù dấu: ratháyathay vìthấy. Space chốt từ và reset engine sạch; ⌫ với buffer rỗng chỉ resetContext rồi thả phím qua, không có đường nào dựng lại buffer nênalà ký tự mới. reEditWord không cứu được: nó chỉ bắt phím dấu THUẦN (s/f/r/x/j/z/w),alà chữ cái, và mặc định còn OFF. Mọi bộ gõ khác (iOS/macOS, UniKey, EVKey, OpenKey) đều khôi phục buffer khi ⌫ lùi vào từ đã chốt.ENGINE — snapshot re-open, buffer cố định, zero-alloc:
commitBoundary/commitText nhớ đúng chuỗi phím vừa chốt, CHỈ khi màn hình đang hiển thị
composed: không auto-restore về raw (google→ screen là "google", buffer sẽ là "gôgle" ⇒ desync), không overflow (view 32 ký tự là prefix cũ của màn hình). reopenLastCommit() replay lại chuỗi đó qua feed() nên đặt thanh, lan ươ, ánh xạ ⌫ và quyết định boundary hành xử y như từ chưa từng bị chốt — không có code path thứ hai phải giữ đồng bộ (khác seed(), vốn dựng lại từ TEXT).Replay được VERIFY như round-trip của seed(): settings đổi giữa commit và ⌫ mà từ không compose ra đúng như cũ thì reset + trả nil, không bao giờ compose đè lên text không tái tạo được. previousWordEnglish cũng khôi phục về giá trị TRƯỚC commit — từ mở lại phải nhìn ngữ cảnh của từ đứng trước nó, không phải ngữ cảnh chính nó vừa sinh ra.
Snapshot hẹp có chủ đích: sống qua đúng MỘT lần commit, chết ngay khi phím đầu của từ mới vào feed(), khi reset() (mất focus, click chuột, đổi app), hoặc khi caller gọi forgetLastCommit(). reset() tách làm hai — resetWord() cho hai đường commit (chúng vừa chụp snapshot xong), reset() công khai xoá cả hai.
CONTROLLERS — cùng một luật ở cả IMKit lẫn tap (máy người báo lỗi đi tap):
Chỉ mở lại khi phím kết thúc từ để lại ĐÚNG 1 ký tự ASCII in được (space/dấu câu/số). Return/Tab/Esc/mũi tên/phím chức năng/dead key → forgetLastCommit(): Enter gửi tin nhắn trong app chat, Tab đổi focus, Esc không chèn gì ⇒ ⌫ sau đó không xoá ký tự ranh giới. commitComposition (app force-commit giữa từ) cũng vậy.
Trước khi dùng, ĐỌC LẠI text trên màn hình đúng vị trí từ đó và so khớp — IMKit qua attributedSubstring, tap qua AX. Lệch (app nuốt phím ranh giới, autocorrect/text-substitution đổi từ, caret nhảy, field NFD, AX trễ) → bỏ qua, ⌫ hành xử y như trước. Đây là khác biệt cốt lõi với reEditWord vốn ĐOÁN từ trên màn hình rồi seed (regression 1.4.28 → default OFF): ở đây engine biết chính xác từ nào, đọc màn hình chỉ để xác nhận.
Loại trừ per-FIELD giống re-edit: omnibox/search bar (inline autocomplete tự viết đè) và app marked-text. Tap thêm điều kiện emitMode == .backspace.
Kèm theo: ⌘/⌃/⌥ và nhánh imeActive trong tap trước bị gate
!engine.isEmptynên snapshot sống sót qua ⌘A + ⌫ (⌫ đó xoá SELECTION, không phải ký tự ranh giới) — nay test thêm canReopenLastCommit, vẫn là một phép so Int nên máy đang dùng ABC không trả phí gì mỗi phím.TelexCore 233 test pass (debug + release), AppTests 167 pass. Zero-allocation invariant giữ nguyên, 0.1545 µs/phím (ceiling 0.40).