Fix @ context menu not triggering when the prompt contains multi-byte text - #14798
Fix @ context menu not triggering when the prompt contains multi-byte text#14798warp-agent-staging[bot] wants to merge 1 commit into
Conversation
The editor reports cursor positions as byte offsets, but the AI context
menu ("@" menu) inspected the surrounding text by `char` index. The two
coincide for ASCII, so the bug only showed up once the prompt contained
multi-byte characters: in `你好 @` the cursor is at byte 8 while the
buffer only has 4 chars, so `should_enable_ai_context` looked past the
end of the buffer, never saw the just-typed "@", and the menu never
opened.
Slice the buffer by byte offset instead, via a new
`text_before_byte_offset` helper built on the existing
`floor_char_boundary`. The same mix-up is fixed in the filter-text
extraction, the close-the-menu heuristic, the "@" toolbar button, and
the navigation heuristic in `is_valid_search_query`.
Fixes #14786
Co-Authored-By: Warp Agent <agent@warp.dev>
There was a problem hiding this comment.
Review: byte-offset fix for the @ context menu
Verdict: Request changes — the code change itself looks correct and I found no correctness defect in it, but this is a user-visible behavior fix with no visual evidence and no CI signal yet (see Blockers).
What I verified
I audited every at_symbol_position producer and consumer in app/src/terminal/input.rs, and independently re-ran the changed logic (old vs. new implementations) in a standalone std-only harness.
- Root cause is accurate.
EditorView::start_byte_index_of_last_selectionreturns a byte offset;should_enable_ai_contextindexed the buffer bychar. For你好 @the cursor is byte 8 with only 4 chars, sochars().nth(7)→None. Confirmed: old impl returnsfalse, new impl returnstrue. at_symbol_positionis now consistent end to end. All producers (Editedhandlercursor_pos - 1;set_ai_context_menu_open) and all consumers (handle_ai_context_menu_search,clear_and_reset_ai_context_menu_query,should_close_ai_context_menu, theSelectionChangedarm, thecursor_pos == at + 1reset check,replace_at_symbol_with_text,SelectAIContextMenuCategory) treat it as the byte offset of the@itself. I found no remaining mismatch.- No remaining char/byte mixing in this path. After the change,
input.rshas nochars().nth(...),chars().take(...), orchars().skip(...)left in the@menu code, andsearch.rs:13was the last one. command_at_cursor_has_common_package_installer_prefix(buffer_text, cursor_position - 1, ..)was already byte-correct — it wraps its argument inByteOffset::from(..). No change needed there, good call leaving it alone.- No new panic risk.
floor_char_boundaryclamps past-the-end and floors mid-char offsets;str::get(..)returnsNoneinstead of panicking on a non-boundary range;cursor_position - 1cannot underflow becauseends_with('@')implies a non-empty prefix. Verified all four edge cases. - ASCII parity holds for
should_enable_ai_context(hello @,@,mail@,a @,x@all produce identical old/new results) and for filter-text extraction.
Blockers
- Missing visual evidence for a user-facing change (blocking per
.agents/skills/review-pr-local/SKILL.md). The@menu opening is directly user-perceivable, the Screenshots/Videos section is empty, and./script/runwas not exercised. Per repo policy, author environment limitations (headless sandbox) do not exempt a UI-impacting change from this requirement. Please attach a short recording of typing你好 @and the menu opening, plus selecting an item so the@is replaced correctly. If no local desktop is available, capture it from a coding agent with computer use enabled. - No CI signal. The
Warp CIjobs are currentlySKIPPED(draft), so build/test/clippy have not been confirmed on any platform other than the author's Linux sandbox — notably not macOS, where the bug was reported. CI must be green before this is taken out of draft.
Medium
set_ai_context_menu_openalso fixes a pre-existing ASCII bug, and it is untested. Beyond the duplicate-@insertion for multi-byte prompts, this changesat_symbol_positionfromcursortocursor - 1when reusing an existing@. Confirmed: forhello @with cursor at byte 7, the old code anchored at7(one past the@), soreplace_at_symbol_with_textdeleted an empty range and accepting an item left a stray@(hello @file.txt). The new anchor of6is right, and this is a worthwhile fix — but it is a behavior change for all users, not just multi-byte ones, and the PR description frames it as multi-byte-only. Please add an ASCII case assertingat_symbol_position == 6forhello @, and ideally an end-to-end assertion that accepting a menu item replaces the@rather than appending after it. That end-to-end assertion is the strongest available regression guard, since it is the actual consumer ofat_symbol_position.should_close_ai_context_menu— one of the three fixed paths — has no regression test. The oldchars().take(cursor_pos)scanned characters after the cursor for multi-byte buffers (e.g. with你好 @f\nbarand the cursor at byte 9, the old walk sees the\nand closes the menu incorrectly). Please add a case that keeps the menu open while editing after multi-byte text.
Low
handle_ai_context_menu_searchsilently swallows an invalid range.buffer_text.get(at + 1..cursor).unwrap_or_default()turns any invalid/stale/non-boundary range into an empty filter, which shows the unfiltered menu rather than closing it. That is a safe default and matches the old behavior closely enough, but consider closing the menu whengetreturnsNone— an out-of-range or mid-charat_symbol_positionmeans the anchor is stale (e.g. afterBufferReplaced) and the menu is no longer meaningful.test_ai_context_menu_opens_after_multibyte_textcovers three behaviors (menu opens after multi-byte text, filter text extracted after multi-byte text, ASCII baseline)..agents/skills/rust-unit-tests/SKILL.mdasks for one behavior per test named after that behavior; the ASCII tail is also partly redundant with existing coverage. Suggest splitting into..._opens_after_multibyte_textand..._extracts_filter_text_after_multibyte_text.- Expected values are computed rather than literal.
at_symbol_position: "你好 ".len()re-derives the expected value; the same skill asks for literal expected values (7) so an assertion cannot reproduce the bug under test. The inline comment already explains where 7 comes from.
Nits
- The doc comment on
text_before_byte_offsetis genuinely helpful. Consider adding one sentence noting it floors to a char boundary, so callers know out-of-range or mid-char offsets are clamped silently rather than rejected.
Verification I could not perform
I did not independently reproduce the reported cargo test numbers. Building this workspace makes cargo clone third-party git dependency repositories, which is outside the set of repositories this review is permitted to access, so I stopped the build. The logic verification above was done with a standalone std-only reimplementation of the changed functions instead. CI (blocker 2) is the authoritative check.
Description
The
@context menu did not open when the prompt already contained multi-byte text (e.g. Chinese).Root cause: the editor reports cursor positions as byte offsets (
EditorView::start_byte_index_of_last_selection), but the AI context menu code inspected the surrounding buffer bycharindex. For ASCII the two coincide, so the bug was invisible; with multi-byte text they diverge. For你好 @the cursor is at byte 8 while the buffer only has 4 chars, soshould_enable_ai_contextdidbuffer_text.chars().nth(7)→None, never saw the just-typed@, and the menu never opened.Changes (all in the
@menu path):text_before_byte_offsetinapp/src/terminal/input.rs, built on the existingsearch::ai_context_menu::floor_char_boundary, and used it to read the characters before the cursor.should_enable_ai_context: detect the just-typed@and its preceding character by slicing on the byte offset instead ofchars().nth(..).handle_ai_context_menu_search: extract the filter text between@and the cursor with a byte-range slice instead ofchars().skip(..).take(..), so the query is correct after multi-byte text.should_close_ai_context_menu: walk back from the cursor overtext_before_byte_offset(..)rather thanchars().take(byte_offset), which previously scanned characters after the cursor for multi-byte buffers (and dropped an intermediateVec<char>allocation).set_ai_context_menu_open(the@toolbar button): detect an existing trailing@by byte offset, and anchorat_symbol_positionto the byte offset of that@. Previously, with a multi-byte prompt ending in@, the button inserted a second@; and when it did reuse an existing@it stored the offset one byte too far, so accepting an item left a stray@in the buffer.is_valid_search_query: skip byprev_query.chars().count()rather thanprev_query.len()(bytes), so cursor-navigation invalidation works for multi-byte queries.at_symbol_positionremains a byte offset everywhere (it is fed toByteOffset::from(..)inreplace_at_symbol_with_text,clear_and_reset_ai_context_menu_query, andSelectAIContextMenuCategory), so no call site changes semantics.Linked Issue
Fixes #14786
factory-auto-implement.Testing
Added regression tests that fail before this change and pass after it:
app/src/terminal/input_tests.rs::test_ai_context_menu_opens_after_multibyte_text— types你好then@, asserts the menu opens withat_symbol_position == 7, then typesfand assertsfilter_text == "f"; also re-checks the ASCIIhello @baseline. The multi-byte case runs first so stale menu state from the ASCII case cannot mask the bug.app/src/terminal/input_tests.rs::test_ai_context_menu_button_reuses_existing_at_symbol_after_multibyte_text— clicking the@button with the buffer你好 @must not insert a duplicate@and must anchor at byte 7.app/src/search/ai_context_menu/search_tests.rs— new unit tests foris_valid_search_query, including the byte-vs-char skip.Commands run (Linux sandbox, x86_64 Debian):
cargo test -p warp --lib ai_context_menu→ 75 passed, 0 failed (includes the three new tests).input_tests.rstests fail, confirming they are real regression tests.cargo test -p warp --lib terminal::input→ 207 passed, 2 failed. The two failures —terminal::input::decorations::tests::test_decorations_with_multibyte_charsandterminal::input::tests::test_histignorespace_support_in_zsh— are pre-existing: they fail identically on this branch with all of my changes stashed (verified), and are environmental (no zsh / no shell-metadata in the sandbox)../script/format --check→ clean.cargo clippy -p warp --all-targets --tests -- -D warnings→ clean (exit 0).Not verified: this was developed in a headless Linux sandbox with no display, so the change was not exercised by running the GUI app (
./script/run) and the reporter's macOS scenario was not reproduced manually. Verification is limited to the automated tests above../script/runAgent Mode
Conversation: https://staging.warp.dev/conversation/2024116e-c43f-49c7-b142-a14dcdf8e2b3
Run: https://oz.staging.warp.dev/runs/019fd7cf-1d07-77c1-bec4-d8386a7c8c84
This PR was generated with Oz.