Skip to content

feat(search): make the workspace index policy visible and correct - #2368

Merged
wgqqqqq merged 1 commit into
GCWing:mainfrom
wgqqqqq:feat/workspace-search-index-strategy
Aug 18, 2026
Merged

feat(search): make the workspace index policy visible and correct#2368
wgqqqqq merged 1 commit into
GCWing:mainfrom
wgqqqqq:feat/workspace-search-index-strategy

Conversation

@wgqqqqq

@wgqqqqq wgqqqqq commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

What this does

Vendors flashgrep v0.2.15, aligns BitFun with the daemon's new base-snapshot protocol, and closes the gaps that left the managed index either computing the wrong thing or impossible to interpret from the UI.

Every number quoted below is measured, not estimated; the methodology and raw runs live outside this PR.

Why the binary and the protocol have to move together

This is a breaking upstream change: RepoStatus.rebuild_recommended is gone, replaced by the base-delta fields; SearchParams.allow_scan_fallback and the BitFun-only QuerySpec.before_context / after_context are gone too.

BitFun declared rebuild_recommended as required, so dropping in the new binary alone makes every status-bearing response fail to deserializeopen_repo, search and glob all become unusable. Confirmed by an stdio probe against the v0.2.12 binary.

The "rebuild recommended" badge loses its data source and becomes "index catching up", driven by base_advance_target_head.

Line text: we were rendering line numbers as content

All four of the daemon's search modes return positions only (LineMatch { path, line_number }, no line text). BitFun rendered the line number as if it were the text, so Grep emitted path:73:line 73 and the desktop search preview showed "line 73".

Content search now goes through search/grouped_line_matches and hydrates text from disk:

  • Local (workspace_search/line_hydration.rs): truncate to max_results before reading, open each file exactly once, do the reads in spawn_blocking.
  • Remote (remote_ssh/workspace_search/remote_line_hydration.rs): the local "one open per file" shape cannot be carried over — on SSH that is one round trip per file, i.e. 250 serial round trips at the default head_limit=250. Instead the request ("which files, which lines of each") is written as a manifest and fed to a single awk pass. 250 matches measured at 4 commands or fewer, typically 1.
  • Both paths share services-core::filesystem::content_preview, so rendering consistency comes from shared code rather than two parallel implementations.

Auto-index threshold: roughly 2000 indexable files

Branch Behaviour Reason
Below threshold Never index 8 ms to 0.2 ms is not perceptible, and small repos have the worst index inflation ratio (3.3x)
Above threshold Build in the background on open 0.66–1.5 s in the middle band; search keeps using the fallback meanwhile, nothing blocks
Very large repos Same, and no confirmation prompt 47 s to 0.1 ms is not an ambiguous trade to hand to the user

The inputs to that decision have asymmetric cost, so the count runs in two commands: git ls-files --cached takes 89 ms on chromium, while adding --others --exclude-standard takes 4250 ms. The tracked pass returns early once the threshold is met and never pays for the untracked walk. Command::output() waits for the process to exit, so an early break only helps if the commands are split.

Builds go through a queue with a cross-workspace disk budget.

The decision has to be visible

The daemon reports needs_index both while the policy is still evaluating a workspace and after it has deliberately declined, so BitFun could only write a sentence that was true either way. The user saw a state that never changed, with no way to tell "stuck" from "nothing should happen here".

The policy's decision now travels with the status (WorkspaceIndexStatus.auto_index), so a small workspace reads "No index needed — only 216 files, below the 2000 threshold; searching directly is faster."

One caveat: the count breaks as soon as the threshold is reached, so Eligible carries a lower bound, not a real count. Only the BelowThreshold branch has a true number — which is exactly the branch that needs to display one. The Eligible copy says "at least N".

Remote SSH has no BitFun-side auto-index policy at all (the remote daemon decides), so it reports None and the frontend falls back to the previous wording.

A non-Git workspace no longer shows a red indicator

flashgrep refuses to open a directory that is not a Git worktree with a HEAD commit. That is a property of the folder, not an index fault, so the raw daemon error is normalized into a stable BitFun-owned sentence, the indicator stays neutral gray, and content search silently falls back.

Grep's -A / -B / -C

The daemon has no context-line support, and the two fields BitFun was sending were its own invention which the daemon ignored — meaning -A / -B / -C were silently doing nothing. Requests asking for context lines now route to ripgrep. render_workspace_search_content_lines is kept for when the daemon gains support.

Verification

  • cargo check --workspace clean
  • cargo test -p bitfun-services-integrations --features workspace-search --lib — 27 passed
  • cargo test -p bitfun-services-integrations --features workspace-search --test workspace_search_contracts -- --ignored — starts a real daemon, indexes a temporary Git repo, and asserts the results carry real line text rather than "line N" placeholders
  • The remote hydration shell_tests feed the generated command to sh -c and run real awk. The same script is byte-identical on BWK awk 20200816 (macOS), GNU Awk 5.4.1 and mawk 1.3.4, covering CRLF, embedded tabs, multibyte text, empty files, out-of-range line numbers, missing files and long-line truncation
  • tsc --noEmit, eslint, vitest run src/tools/file-explorer (12 passed)
  • Locale key sets identical across the three locales (114 each)

Not included

  • The performance investigation write-up and the architecture doc update are kept out of this PR deliberately.
  • 17 files whose only change is rustfmt line rewrapping (relay-service/db.rs, terminal/transcript.rs, and others) are unrelated to this work and were left out.

@wgqqqqq
wgqqqqq force-pushed the feat/workspace-search-index-strategy branch from d768261 to 6efba7d Compare August 18, 2026 10:35
Vendors flashgrep v0.2.15, aligns BitFun with the daemon's new base-snapshot
protocol, and closes the gaps that made the managed index either wrong or
inscrutable from the UI.

Protocol (breaking upstream change): `RepoStatus.rebuild_recommended` is gone,
replaced by base-delta fields; `SearchParams.allow_scan_fallback` and the
BitFun-only `QuerySpec.before_context`/`after_context` are gone too. The old
required field would have failed every status-bearing response, so the binary
bump and the protocol change land together. The "rebuild recommended" badge
becomes "index catching up", driven by `base_advance_target_head`.

Line text: the daemon's four search modes return positions only, so BitFun was
rendering line numbers as content ("path:73:line 73"). Content search now goes
through `search/grouped_line_matches` and hydrates text from disk, locally via
`workspace_search/line_hydration.rs` and over SSH via a single batched awk pass
(`remote_line_hydration.rs`) rather than one round trip per file. Both paths
share `services-core::filesystem::content_preview`.

Auto-index policy: index only workspaces with roughly 2000+ indexable files.
Below that the measured win is 8 ms to 0.2 ms while the index inflates worst
(3.3x). The file count is gathered in two commands because
`--others --exclude-standard` costs 4.2 s on chromium against 89 ms for
`--cached`, so the tracked pass returns early once the threshold is met.
Builds run through a queue with a cross-workspace disk budget.

Policy visibility: the daemon reports `needs_index` both while the policy is
still evaluating and after it declined, so the UI could only hedge. The
decision now rides along with the status (`WorkspaceIndexStatus.auto_index`),
and a small workspace reads "no index needed" with the count and threshold
instead of an ambiguous sentence that never changes. Remote workspaces have no
BitFun-side policy and report `None`.

A workspace that is not a Git worktree can never be indexed, which is a
property of the folder rather than a fault, so it stays on the neutral
indicator instead of turning red.

Grep's `-A`/`-B`/`-C` route to ripgrep, because the daemon has no context-line
support and the flags were previously dropped on the wire without a word.
`ContentSearchRequest` loses its context-line fields entirely rather than
keeping them unread: a field that looks like it carries context but is silently
discarded is exactly how the original defect happened, so the indexed path can
no longer express the request at all.
@wgqqqqq
wgqqqqq force-pushed the feat/workspace-search-index-strategy branch from 6efba7d to c928460 Compare August 18, 2026 11:00
@wgqqqqq
wgqqqqq merged commit 24b2a68 into GCWing:main Aug 18, 2026
8 checks passed
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