* fix(pi-fff): preserve path and exclude constraints in fuzzy grep fallback (#697)
The automatic fuzzy fallback in the pi-fff `grep` tool passed the raw
`pattern` to `picker.grep`, discarding the constrained `query` built via
`buildQuery`. As a result, the fallback ignored the caller's `path` and
`exclude` constraints and could return matches from explicitly excluded
directories or files outside the requested path.
Pass the constrained `query` to the fallback instead, so it only broadens
matching (fuzzy vs. plain) without broadening scope.
Closes#697
* fix(pi-fff): drop path constraint in fuzzy fallback only for file paths
When the caller pinned a specific file (path has an extension), the
fuzzy fallback broadens across the whole picker so a mistyped filename
can still surface matches. For directory constraints (or no path), keep
the constrained query so the fallback does not leak matches from
excluded / out-of-scope directories.
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
LMDB envs may only be opened once per process. AuxFinderPool was forwarding
the main finder's frecencyDbPath / historyDbPath into every aux FileFinder,
so the first out-of-workspace search failed with
"environment already open in this program".
Aux finders are transient and per-search; they run without persistent
frecency/history scoring.
Closes#700
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Adds @ff-labs/fff-bin-android-arm64 platform package and wires the
existing aarch64-linux-android CI build into the npm publishing matrix.
Extends getTriple() in fff-node and fff-bun to map process.platform
'android' to linux-android, and adds android to os arrays with the new
optionalDependency. Closes#692.
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
On Windows, `path.relative()` returns an absolute path when the source
and target are on different drives (e.g. `D:\` → `C:\`). The existing
check in `routePathConstraint()` only recognizes `".."` and `"..\..."`
as indicators that a path is outside the workspace — it misses the
cross-volume case entirely. The path is then treated as workspace-local,
and downstream code rejects it with:
Path constraint must be relative to the workspace
Extract the workspace-outside check into
`isOutsideWorkspaceRelativePath()` so the logic is testable in
isolation, and add `path.isAbsolute()` to the condition. A cross-volume
relative result is by definition outside the workspace.
Add a Windows-specific regression test that verifies the helper
recognizes a cross-volume `path.win32.relative()` result as outside the
workspace. The test is gated on `process.platform === "win32"` and has
no effect on Linux or macOS CI runs.
PR #669 made sdk.ts dynamically import @ff-labs/fff-bun when running under
bun, but only added it as an optional peer dependency. Package managers do
not install optional peers by default, so `pi install npm:@ff-labs/pi-fff`
on a bun-only host produced a node_modules tree without @ff-labs/fff-bun
and pi crashed at session_start with "Cannot find module '@ff-labs/fff-bun'".
Promote both SDKs to regular deps. Bundle size cost is minor (SDKs are TS
shims over the same optional native bins) and this guarantees the dynamic
import in sdk.ts always resolves regardless of runtime or installer.
Closes#689
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* fix(pi-fff): route out-of-workspace path constraints to a rotating aux finder pool
Hotfix prototype for #463. When the agent passes an absolute `path`
outside the workspace cwd to ffgrep/fffind, spin up (or reuse) a
FileFinder rooted at that path instead of throwing
"Path constraint must be relative to the workspace".
Pool keeps at most 3 aux finders, LRU-evicted, dropped after 5 minutes
of inactivity. Find pagination cursors carry the aux root so resumes
hit the same finder.
* fix: Redesign the aux finder
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
* fix(picker_ui): highlight fuzzy query matches on file picker
* fix(file-picker): gate fuzzy query highlights behind config
* fix(file-picker): use rust fuzzy match ranges for highlights
Compute fuzzy match byte ranges on the Rust side for paginated file results and pass them through the nvim Lua binding.
* fix(pi-fff): isomorphic runtime detection with lazy SDK load
Detect bun vs node at runtime and dynamically import the matching SDK
(@ff-labs/fff-bun on bun, @ff-labs/fff-node on node) via a variable
package name so oh-my-pi's static extension validator does not chase
ffi-rs' optional native binaries through the module graph.
Closes#668
* fix(pi-fff): fail loud on wrong-runtime SDK
Drop the cross-runtime SDK fallback in loadSdk. Falling back to the
node SDK on a bun host re-introduces the ffi-rs cost this PR is meant
to avoid, and hides packaging bugs where the correct SDK is missing.
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
`fff-search` supports two mutually exclusive backend features (`ripgrep`
default, or `zlob`) but internal cfg gates keyed off
`cfg(not(feature = "zlob"))`, meaning the ripgrep code compiled even
when the `ripgrep` feature (and its `ignore`/`globset` deps) was off.
Building with `--no-default-features` produced a wall of
"unresolved crate" errors.
Tighten the gates to `all(not(feature = "zlob"), feature = "ripgrep")`
and add a top-level `compile_error!` when neither backend is enabled,
so the failure is a single actionable message instead of 15 cascading
errors.
Refs #659
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* fix(core): restrict AVX2 normalize to x86_64
The AVX2 path in `normalize_bytes` gated on `any(target_arch = "x86_64",
target_arch = "x86")` unconditionally imports `std::arch::x86_64`, which
does not exist on 32-bit x86, so `fff-search` failed to compile for
`i686-unknown-linux-gnu` (e.g. termux i686 android builds pulling us in
via nushell). SIMD stays on x86_64/aarch64; 32-bit x86 falls back to the
scalar path.
Add a `Build i686-unknown-linux-gnu` CI job so this regresses loudly next
time. Closes#656.
* fix(core): silence unused i1/i2 on non-SIMD targets
`select_rare_pair` results are only consumed inside x86_64/aarch64 cfg
blocks, so on 32-bit x86 both bindings are unused and CI's
`-D unused-variables` fails the i686 build introduced in the previous
commit.
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* fix(simd_path): clamp resolve_ptrs iteration to buf.len()
resolve_ptrs() iterates self.indices.len() times over a fixed-size
[*const u8; 32] buffer with no guard. When a file path exceeds 512
bytes (32 chunks × 16 bytes), the loop accesses buf[32] and panics:
index out of bounds: the len is 32 but the index is 32
On macOS PATH_MAX is 1024, so any legitimately long path can trigger
this. Clamp count to buf.len() so pathological paths are truncated
gracefully instead of crashing.
Fixes an OOB panic found in pi-fff v0.9.6.
* fix(score): size chunk ptr buffers to PATH_MAX instead of 512 bytes
The scoring hot path passed fixed [*const u8; 32] buffers (32 * 16 =
512 bytes) to resolve_ptrs while PATH_BUF_SIZE allows PATH_MAX-long
paths (1024 on macOS, 4096 on Linux), so any path over 512 bytes
panicked with an out of bounds index.
neo_frizbee 0.10.4 makes the resolver buffer size a const generic, so
the buffers are now sized MAX_PATH_CHUNKS = PATH_BUF_SIZE / 16 at
compile time and long paths are matched in full instead of truncated.
Also covers the frizbee greedy fallback for haystacks longer than its
DP matrix which previously scanned a stale score matrix and panicked.
Adds regression tests for both the resolve_ptrs unit level and the
full scoring pipeline.
---------
Co-authored-by: elee7420-gif <elee7420@gmail.com>
pi forks (e.g. omp) do not expose addAutocompleteProvider, causing
session_start to throw "FFF init failed: ... is not a function" and
skipping tool registration entirely.
Skip UI wiring when the host lacks the method; tools still register.
Refs #651
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
SharedFrecency and SharedQueryTracker are now type aliases over a single
SharedDb<T: LmdbStore>, and the three wait_for_* methods share one
poll_until helper. Public API unchanged.
* feat: Zlob walker
* feat: Use outcome gitignore rules
; Conflicts:
; crates/fff-core/src/file_picker.rs
; crates/fff-core/tests/scan_correctness_contract.rs
* chore: get rid of ripgrep crates in default build
* refactor: use '/' as canonical internal path separator
The zlob walker emits '/'-separated relative paths on every platform.
Instead of converting every walked path to native '\' on Windows, make
'/' the canonical internal separator throughout the index and convert
native inputs to '/' on the (fewer) inbound lookup paths.
Native separators are re-applied only at OS/state boundaries:
- write_absolute_path nativizes for git-cache keys, frecency, Win32 APIs
- frecency keys additionally canonicalize on Windows (dunce), with a
raw-string fallback so watcher deletes never drop the op
Removes several scattered Windows compensations (score.rs fuzzy folds,
constraints.rs collect fold) now that stored paths are already '/'.
relative_path emitted to Lua is now '/' on all platforms.
The preview window hardcoded 'IncSearch' for grep match highlights while
the results list uses config.hl.grep_match. Overriding grep_match had no
effect on the preview, forcing users to override the global IncSearch
group instead.
Refs #646
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
* feat: expose follow_symlinks for SDK/MCP/python paths
Closes#627
Bumps FFF_CREATE_OPTIONS_VERSION to 2 (append-only).
Adds --follow-symlinks flag to fff-mcp, follow_symlinks kwarg to the
pyo3 finder, and followSymlinks option to @ff-labs/fff-node. Default
stays false everywhere; follow_symlinks is preserved across reindex
in C and Python.
Watcher cycle handling is unchanged — caller must ensure the indexed
tree has no symlink loops.
* fix build
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
* docs(nvim): add visual-mode example for fc keymap (#631)
Refs #631
* feat(api): add live_grep_under_cursor for normal/visual mode
* refactor(nvim): live_grep_under_cursor reads visual region without yank
Use getpos('<)/getpos('>) + getregion() instead of yanking into the v register.
Avoids touching the user's registers entirely.
* chore: improve default binding
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
* fix(mcp): self-exit after idle timeout to mitigate orphan processes
Some MCP clients (e.g. Claude Code) spawn fff-mcp per session but don't
close stdin on session end, leaving orphan processes consuming CPU
(#633, #497).
Add an idle watchdog: each tool call bumps a last-activity timestamp, a
background task ticks every 60s and exits(0) after --idle-timeout-secs
(default 900) without activity. Set to 0 to disable. Also configurable
via FFF_MCP_IDLE_TIMEOUT_SECS.
Stopgap before the daemon-based process model. Notifications/pings do
not count as activity, so a chatty-but-dead client cannot keep the
process alive forever.
* fix: cleanup
---------
Co-authored-by: gustav-fff <286169375+gustav-fff@users.noreply.github.com>
Co-authored-by: Dmitriy Kovalenko <dmtr.kovalenko@outlook.com>
* fix(nvim): keep prompt icon when clearing input with cc/S
In prompt_vim_mode the input bar is a prompt buffer whose prompt icon is
protected text at the start of the line. A normal-mode cc/S deletes the
whole line including the prompt, so Neovim re-inserts the icon as plain
literal text that has to be backspaced out.
Map cc and S to reset the line to just the prompt and re-enter insert at
the right column, so clearing the query leaves the prompt intact.
* fix(nvim): allow remap of cc/S clear bindings in prompt vim mode
Pass remap=true so existing user mappings of cc/S still resolve through
our handler, per review feedback.