Skip to content

feat(tui): @-mention autocomplete for file/folder context input#290

Merged
quangdang46 merged 3 commits into
masterfrom
feature/at-mention-context
May 24, 2026
Merged

feat(tui): @-mention autocomplete for file/folder context input#290
quangdang46 merged 3 commits into
masterfrom
feature/at-mention-context

Conversation

@quangdang46
Copy link
Copy Markdown
Owner

Summary

Implements @<path> autocomplete in the TUI input with Claude Code parity. Type @ anywhere in chat → dropdown shows file/folder suggestions ranked by frecency; pick a folder to drill in, pick a file to inline its content on submit.

What works

  • @ triggers picker dropdown anywhere in input (cursor-based detection, not just end-of-input)
  • File suggestions insert @path and inline full content on submit (cap: 200 KB)
  • Folder suggestions insert @path/ and KEEP the dropdown open for drill-in; on submit, expand to a 1-level directory listing (cap: 1000 entries, dirs sorted before files)
  • Quoted paths @"path with spaces" for paths containing spaces
  • Line ranges @file#L10 and @file#L10-20
  • Multiple mentions per message all expanded; email-like user@example.com correctly skipped
  • Frecency-ranked results — repeatedly-mentioned paths float to the top, persisted to ~/.jcode/cache/ffs/<repo-hash>/

How it works

Three layers, one per commit:

  1. AtPicker wraps ffs-search 0.1.3. Background scan + filesystem watcher rooted at session.working_dir, gitignore-aware via ffs's ignore integration, fuzzy match via neo_frizbee, frecency persisted via LMDB. Lazy AtPickerSlot state machine (Pending/Initialized/Failed) so we don't spawn threads in test/replay modes.

  2. Cursor-based token detection in state_ui_input_helpers.rs. New extract_at_token_at_cursor(input, cursor) → Option<AtTokenMatch> returns byte offsets so callers can do exact in-place replacement. Handles quoted paths, unicode (CJK, accents), multiple @s in one input. The command_suggestions() entry point checks for an active @-token first; if found, suggestions come from AtPicker::search() and the slash/dollar branches don't run. The autocomplete() (Tab) handler does the in-place token replacement: file → trailing space (close), folder → trailing slash (drill-in), space-containing paths → quoted form.

  3. Submission-time expansion in input.rs. expand_at_path_references() rewritten as a byte-index state machine. Folders inline a sorted listing (matches Claude Code's behavior — no recursive content inlining, just direct children with truncation note). Line ranges parsed off the end of the token so paths containing # work. Invalid ranges leave the token literal so the user can fix the typo.

Spec source

UX choices match claude-code-best/claude-code source (verified, not docs):

  • MAX_DIR_ENTRIES = 1000src/utils/attachments.ts:1973
  • applyDirectorySuggestion: file → ' ', dir → '/'src/hooks/useTypeahead.tsx:277-279
  • 1-level readdir, no recursive content inlining — src/utils/attachments.ts:1968-1995
  • MAX_SUGGESTIONS = 15src/hooks/fileSuggestions.ts

Tests

41 new tests across 3 modules:

Module Tests What's covered
at_picker::tests 8 state machine, search, dirs-only mode, idempotent warm_up, multi-picker no-deadlock, stable cache keys
state_ui_input_helpers::dollar_token_tests (cursor section) 21 start/middle/end positions, multiple consecutive tokens, email rejection, quoted forms (closed/unclosed/cursor-mid-quote), unicode paths, slash separators, whitespace boundaries
input::at_path_tests 12 quoted paths, line ranges (single/multi/clamped/invalid), folder listing sort, folder cap + truncation note, multiple mentions, email bystander, folder w/o trailing slash

Plus 26 pre-existing tests in those modules unchanged and passing.

Verification

  • cargo check --lib
  • cargo check --tests ✅ (compiles every test in the workspace, no regressions elsewhere)
  • Targeted test runs for the 3 modules ✅ (66/66 passing)

Out of scope (separate PRs)

  • MCP resource mentions @server:resource/path
  • Agent mentions @agent-name
  • Drag-drop file → terminal
  • Image attachment via @image.png
  • ffs-search version bump strategy (currently pinned =0.1.3)

Adds a session-scoped `AtPicker` that wraps ffs-search 0.1.3's FilePicker
to power `@<path>` autocomplete in the chat input. Features:

- Background filesystem scan + watcher rooted at session.working_dir
- Fuzzy matching with typo resistance (via neo_frizbee inside ffs)
- Frecency ranking persisted to ~/.jcode/cache/ffs/<repo-hash>/
- Mixed file/dir search; trailing `/` activates dirs-only drill-in mode
- Lazy init via AtPickerSlot enum (Pending/Initialized/Failed) so we
  don't spawn threads in test-harness or replay modes

Adds 8 unit tests covering: state machine, search filtering, dir-only
mode, idempotent warm_up, multi-picker no-deadlock, and stable cache
keys for distinct repos.

Picker is wired into App via a new `at_picker: RefCell<AtPickerSlot>`
field; consumers come in follow-up commits.
Implements Claude-Code-style @-mention UX:

- New `extract_at_token_at_cursor()` finds an active @-token at any
  position in the input based on cursor location, not just end-of-input.
  Supports quoted paths `@"path with spaces"` and unicode (CJK,
  accents).
- `active_at_token()` (end-of-input only) is kept for backward compat
  with existing tests.
- New `AtTokenMatch` struct exposes byte offsets so callers can do
  exact in-place replacement.
- `command_suggestions()` now checks for an active @-token first; when
  found, it returns ffs-search results formatted as `@path` for files
  or `@path/` for folders. `/` and `$` namespaces fall through.
- `autocomplete()` (Tab) handles @-tokens via new
  `try_apply_at_completion()` helper:
    * file selection → insert `@path ` (trailing space, close token)
    * folder selection → insert `@path/` (keep token open for drill-in)
    * paths containing spaces → quoted form `@"..."` automatically
  Replacement uses byte offsets from AtTokenMatch so mid-input mentions
  preserve text on either side.

Adds 21 cursor-based detection tests covering: start/middle/end of
input, multiple consecutive tokens, email-like rejection, quoted
forms (closed/unclosed/cursor-mid-quote), unicode paths, slash
separators, whitespace boundaries.
Extends submission-time @-mention expansion to match Claude Code:

- @path/to/folder (with or without trailing /) now inlines a 1-level
  directory listing instead of being treated as a missing file.
  Listing format:
    --- @folder/ (directory listing, N entries) ---
    subdir1/
    subdir2/
    file_a.rs
    file_b.rs
    --- end @folder/ ---
  Sort order: directories first, then files, alphabetical within each
  group (matches Claude Code's claude-code-best/claude-code source).
  Capped at AT_FOLDER_MAX_ENTRIES=1000 with explicit truncation note;
  no file content is inlined for folder mentions.

- @path#L10 and @path#L10-20 line range syntax. Range is parsed off
  the END of the token so paths containing # work. Invalid ranges
  (zero, backwards, non-numeric) leave the token literal so the user
  can fix the typo.

- @"path with spaces" quoted form now expands correctly. The token
  is taken verbatim between quotes; trailing punctuation stripping
  is bare-token-only.

The expansion loop is rewritten as a byte-index state machine for
cleaner handling of multiple mentions and quoted boundaries.

Adds 12 tests: quoted paths, single/multi-line ranges, range
clamping, invalid range fallback, folder listing sort order, folder
cap + truncation note, multiple mixed mentions, email-bystander
non-expansion, folder w/o trailing slash.
@quangdang46 quangdang46 merged commit b5914e3 into master May 24, 2026
@quangdang46 quangdang46 deleted the feature/at-mention-context branch May 24, 2026 22:11
quangdang46 added a commit that referenced this pull request May 24, 2026
After merging 13 PRs in sequence (#290 + #292-#303), two squash-merges
collided on shared schema state:

1. crates/jcode-provider-metadata/src/catalog.rs:
   - Both VERTEX_AI (#283, prior) and BIGMODEL (#297) added entries.
   - Both #19 (#263 GitLab Duo, prior) and #297 declared array size,
     each thinking they were +1 from baseline. Net actual: +2 from
     baseline but final declared count was off by 1.
   - Fixed: OPENAI_COMPAT_PROFILES 35→36, LOGIN_PROVIDERS 48→49 to
     match actual entry count.

2. src/cli/commands/provider_setup.rs:
   - PR #299 added headers: BTreeMap<String, String> field to
     NamedProviderConfig but didn't update this constructor (which
     pre-dated #299 in master).
   - Fixed: added headers: BTreeMap::new() in the configure
     constructor.

cargo check -p jcode --bin jcode now clean. No behavioral change
beyond what each PR brought.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant