Skip to content

feat(tui): @file mention picker with frecency ranking (closes #570) - #1268

Open
alecuba16 wants to merge 6 commits into
1jehuang:masterfrom
alecuba16:select_files
Open

alecuba16 wants to merge 6 commits into
1jehuang:masterfrom
alecuba16:select_files

Conversation

@alecuba16

Copy link
Copy Markdown

Closes #570.

Problem

When composing prompts in the TUI there is no way to quickly reference files by name. Users must type full paths by hand, which is slow and error-prone in large codebases. #570 tracks this; maintainer guidance there suggested starting with project-local tracked files and a deterministic recency/frequency score, which is what this PR implements.

What it does

Typing @ in the composer opens a file-completion popover ranked by frecency (frequency + exponential recency decay). Accepting a completion replaces the @query with the path and records an inline file chip; backspace on a chip deletes the whole path token instead of one character.

  • Indexing: git ls-files --cached --others --exclude-standard as the primary source, with a walkdir fallback honoring .gitignore for non-git directories. Rebuilt in the background with adaptive TTL (30s base, 4x for large workspaces) and a max_files safety cap. A notify-based watcher marks the index dirty on filesystem changes.
  • Matching: case-insensitive substring matching behind a compact CharBag pre-filter, then a regex pass for path-segment matching. Gitignored directories are scanned lazily via fs::read_dir only when a query actually targets them.
  • Popover UX: results grouped into Recent / Files sections. The selection never rests on a section header (the highlight and the Enter-accept path always resolve to the same real row), and the building-index hint row is a non-acceptable sentinel so Enter can never insert it as a chip.
  • Config: tunable via [file_mention] in ~/.jcode/config.toml (refresh_ttl_secs, max_results, max_files; zero or absent values fall back to built-in defaults), documented in README and shown in the /config settings summary.

Edge cases covered by tests

  • Section-header up-skip wraps correctly for non-power-of-2 list lengths (regression: wrapping_sub(1) % len computed usize::MAX % len).
  • The first-use building-index hint row is rejected by the accept guard and filtered from Tab-completion targets.
  • Out-of-range popover selection clamps, all-header lists terminate, empty lists are safe.
  • Config: absent section, partial section, zero values, TOML round-trip, and cap enforcement all fall back to documented defaults.
  • Index behavior: binary-file filtering, gitignore loading/matching, empty-dir queries, lazy ignored-dir scan.

Tradeoffs / assumptions

  • frecency state persists per-project on disk (frequency + last-access time), mirroring the approach of editor file pickers; it does not reuse session history because ranking must survive across sessions.
  • The CharBag pre-filter is an allocation-light first pass; the regex pass only runs on surviving candidates to bound per-keystroke cost.
  • Suggestion reads are memoized per frame and the config accessor is cached, so the per-keystroke path does not touch disk config or re-rank on every render.

Validation

  • jcode-tui file_mention/input_ui/suggestions battery: 111 passed.
  • Root-crate commands_tests compile fix included (master commit ff32973 added a usage field to ModelRoute without updating 5 test initializers, breaking cargo test -p jcode; also available standalone on fix/modelroute-usage-test-init if preferred as a separate PR).
  • provider_init_tests env isolation so provider tests are deterministic across developer machines.
  • Workspace cargo check --all-targets clean; clippy clean for all files touched by this PR; runtime smoke test on a standalone socket passes.

…ention] config

Add an @file mention picker to the TUI composer: type @path to get
frecency-ranked file completions in a popover. Accepted files become inline
chips in the input box; backspace on a chip deletes the whole token.

Indexing uses git ls-files with a walkdir fallback, rebuilt in the background
with adaptive TTL and a max_files safety cap. Queries go through a CharBag
pre-filter plus a regex pass; gitignored directories are scanned lazily when
a query targets them. The popover groups results into Recent and Files
sections; the selection never rests on a section header and the building-index
hint row is never acceptable.

Tuning is exposed through the [file_mention] config section (refresh_ttl_secs,
max_results, max_files; zero or absent falls back to built-in defaults),
documented in README and shown in the /config settings summary.

Also fixes a section-header up-skip wrap bug for non-power-of-2 list lengths
and uses the cached config() accessor on the per-keystroke refresh path.
@greptile-apps

greptile-apps Bot commented Sep 15, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

No blocking issues remain; the change is safe to merge.

Findings

  1. P2 File Limits Apply Too Late
Fix with agent prompt
### Issue 1
crates/jcode-tui/src/tui/app/file_mention.rs:1655-1668
The prompt builder synchronously reads every selected text file in full before checking the 100 KB per-file limit, then applies the 500 KB cumulative budget only after all file contents have been collected. This is a non-blocking concern, but a large selected file can still block the UI and allocate its full contents before either limit takes effect.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR adds @file mention completion to the terminal UI, including workspace indexing, ranked suggestions, inline file chips, project-scoped frecency, bounded file-context loading, and configuration controls. The follow-up changes address Unicode-safe prefix handling, inactive mention parsing, attachment restoration during undo, project-scoped history, and invalid UTF-8 reporting.

Reviews (3) · Last reviewed commit: "fix(tui): report corrupt @file reads ins..."

Comment thread crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs
Comment thread crates/jcode-tui/src/tui/ui_input.rs Outdated
Comment on lines +1655 to +1668
match std::fs::read_to_string(path) {
Ok(content) => {
let block = if content.len() <= MAX_FILE_SIZE {
content
} else {
let line_count = content.lines().count();
let preview: String = content.lines().take(200).collect::<Vec<_>>().join("\n");
format!(
"{}\n\n[... file too large: {} lines, {} bytes, showing first 200 lines]",
preview,
line_count,
content.len(),
)
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 File Limits Apply Too Late

The prompt builder synchronously reads every selected text file in full before checking the 100 KB per-file limit, then applies the 500 KB cumulative budget only after all file contents have been collected. This is a non-blocking concern, but a large selected file can still block the UI and allocate its full contents before either limit takes effect.

Knowledge Base Used: Session storage and context management

Artifacts

Evidence from the check

  • This authored script temporarily adds a narrow test that creates a size-controlled selected text file, invokes the real prompt builder, verifies the complete-size marker, and restores the source; it demonstrates the tested path.

Command output from the check

  • This completed command capture runs the focused test against a 614,400-byte selected file and shows the exact full input size in the truncation marker despite the 100 KB and 500 KB limits; the takeaway is that the full file was read before limiting.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/jcode-tui/src/tui/app/file_mention.rs
Line: 1655-1668

Comment:
**File Limits Apply Too Late**

The prompt builder synchronously reads every selected text file in full before checking the 100 KB per-file limit, then applies the 500 KB cumulative budget only after all file contents have been collected. This is a non-blocking concern, but a large selected file can still block the UI and allocate its full contents before either limit takes effect.

**Knowledge Base Used:** [Session storage and context management](https://app.greptile.com/solo-systems/-/custom-context/knowledge-base/1jehuang/jcode/-/docs/session-storage-and-context-management.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread crates/jcode-tui/src/tui/app/input.rs
Comment thread crates/jcode-tui/src/tui/app/file_mention.rs
Operate on char vectors and collect the prefix from chars instead of
using a char count as a byte offset. Also end file-mention mode once
the user types a space after the @token, so a stale mention can no
longer drive suggestions or intercept Enter.
Ctrl+Z snapshots now carry file_chips, so undoing a backspace that
pruned a chip brings the attachment back instead of silently dropping
it from the prompt at send time.
Use metadata plus a bounded read for oversized chips so a huge file is
never fully loaded just to be truncated. The preview still shows the
first 200 lines.
Ranking signals from one repository are noise in another, so the
frecency log now lives under state_dir/file_frecency/<hash-of-cwd>/.
check_refresh swaps to the new project's store when the cwd changes.
@alecuba16

Copy link
Copy Markdown
Author

Addressed all 5 Greptile findings in follow-up commits (83c4ac7..108b13b):

  1. UTF-8 panic in common_prefix — now operates on char vectors and builds the prefix from chars. Regression test covers éx/éy, café/x.rs, and divergence inside a multi-byte char.
  2. @mention stays active after a spaceextract_at_query now requires the @token to extend to the end of the input, so typing @main then continue no longer leaves a stale mention intercepting Enter. New tests pin the boundary cases.
  3. Undo loses the file chip — Ctrl+Z snapshots (InputUndoEntry) now carry file_chips, so undoing a backspace restores the attachment instead of silently dropping it at send time. Test added.
  4. Full read before size check — chips now stat the file first and use a bounded read (read_to_string_bounded); oversized files are never fully loaded. Tests cover oversize truncation and the read cap.
  5. Frecency leaks across projects — the log is now keyed by the working dir (state_dir/file_frecency/<sha256(cwd)>/file_frecency.jsonl) and check_refresh swaps stores when the cwd changes. Isolation test added.

Comment thread crates/jcode-tui/src/tui/app/file_mention.rs Outdated
read_to_string_bounded now only keeps the truncated prefix when the
byte limit sliced a multi-byte char at the cut. Any invalid UTF-8
before the limit is treated as corruption and surfaces the existing
read-failure marker, so the model never sees silently truncated file
context.
@alecuba16

Copy link
Copy Markdown
Author

Fixed the follow-up P1 (Corrupt Files Lose Context) in 8d5a0ee.

read_to_string_bounded now distinguishes the two cases explicitly:

  • Byte limit slicing a multi-byte char at the cut (file itself is valid): keep the truncated prefix.
  • Invalid UTF-8 anywhere before the limit (genuine corruption): return InvalidData, which flows into the existing [read failed: …] marker in the prompt.

Tests added: mid-file invalid byte rejected at the reader level, sliced-char-at-limit prefix kept, and end-to-end build_prompt_with_files asserting the failure marker appears and no partial content is embedded.

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.

feat(tui): file selection mentions with frecency ranking

1 participant