Skip to content

feat(desktop): Cmd+P と Files タブの検索ロジックを VSCode 準拠に刷新 (#359)#365

Merged
MocA-Love merged 6 commits intomainfrom
fix/file-search-vscode-parity
Apr 20, 2026
Merged

feat(desktop): Cmd+P と Files タブの検索ロジックを VSCode 準拠に刷新 (#359)#365
MocA-Love merged 6 commits intomainfrom
fix/file-search-vscode-parity

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 20, 2026

Summary

右サイドバー Files タブと Cmd+P の「重い」「欲しいファイルが出てこない」問題を、VSCode Quick Open (anythingQuickAccess.ts + fuzzyScorer.ts) のロジックに寄せて根本対応。

Issue: #359

なにが変わるか

症状 before after
dist/, build/, coverage/ 配下のファイルが検索でヒットしない ハードコード除外 .gitignore 尊重 (VSCode と同等)
.gitignore されたファイルを狙って探せない 出ない includeHidden=true で全部出る
大規模リポで最初のキー入力が遅い 初回クエリで同期インデックス構築 Cmd+P オープン時に warmup
検索中のキー入力でスコアリングが詰まる キャンセル不可 AbortController で中断
最近開いたファイルが上位に来ない UI で別セクションだけ VSCode 準拠の score boost
開いているファイルが埋もれる ブースト無し +2000 score boost
Files タブの検索レスポンスが不規則 toolbar 150ms + hook 150ms の二重 debounce hook 側 150ms に一本化

主な変更点

packages/workspace-fs/src/search.ts

  • buildSearchIndexripgrep --files ベースに切替、fast-glob フォールバック付き
    • .gitignore / .rgignore / .git/info/exclude / nested gitignore を自動尊重
    • DEFAULT_IGNORE_PATTERNS.git/ + node_modules/ のみに縮小
    • FALLBACK_IGNORE_PATTERNS で fast-glob 時のみ dist/build/.next/.turbo/coverage を追加除外
  • searchFilesopenFilePaths / recentFilePaths / signal / runRipgrep パラメータを追加
    • VSCode 準拠の range boost: open file +2000, MRU +1000, recency tiebreaker
  • AbortController ベースのキャンセル機構を追加 (workspace 単位)、スコアリングループに yield ポイント
  • warmupSearchIndex API をエクスポート
  • Fuse.js 依存 + デッドコード (_collectExactFileSearchMatches, compact index maps 等) を削除

tRPC / service 層

  • core/service.ts, host/service.ts, client/index.ts に新パラメータ + warmupSearchIndex を追加
  • desktop / host-service の両 tRPC ルータで新スキーマと warmup プロシージャを公開

UI (apps/desktop)

  • useFileSearch / useWorkspaceFileSearchopenFilePaths / recentFilePaths / includePattern / excludePattern を追加 (feature parity)
  • useCommandPalette で MRU / open の絶対パスを受け取り searchFiles に転送、ダイアログ初回オープン時に warmup
  • v2-workspace/page.tsx から open/recent の絶対パスリストを useCommandPalette に渡す
  • FileTreeToolbar の自前 debounce を削除 (hook 側 useDebouncedValue(150) に一本化)

テスト

  • searchFiles: open file boost / MRU tiebreaker / warmup の 3 ケース追加 (計 35 pass)

設計メモ

  • MRU/open boost は v2 workspace 内 のみ適用。Cmd+P のグローバル検索 (scope=global) では他ワークスペースの recency を引きずらないよう、multi-workspace 検索には MRU を渡していない
  • DEFAULT_IGNORE_PATTERNS は patch event (shouldIndexRelativePath) でも使われるため最小限に。TTL 切れ時の rebuild で ripgrep による再フィルタがかかるので整合性は担保される
  • ripgrep 未インストール環境 (開発機 / CI フォールバック) でも動作するよう fast-glob フォールバックを維持

Test plan

  • Cmd+P を開いて warmup が走ることを確認 (devtools で warmupSearchIndex mutation)
  • Cmd+P で最近開いたファイルが上位に来ることを確認
  • Cmd+P で現在開いているタブのファイルが最上位に来ることを確認
  • .gitignore に書かれたファイルがデフォルトで検索結果に出ないことを確認
  • Files タブで高速に入力しても詰まらないことを確認
  • bun test / bun run typecheck / bun run lint 全通過 (確認済)

Summary by CodeRabbit

リリースノート

  • New Features

    • ファイル検索がアクティブなファイルと最近使用したファイルに基づいて改善された結果ランキングをサポートしました。
    • 検索インデックスのウォームアップ機能を追加し、初期検索のパフォーマンスを向上させました。
  • Performance Improvements

    • ファイル検索の実装を最適化し、より高速な検索を実現しました。
  • Bug Fixes

    • ファイル検索エラーハンドリングが強化されました。

右サイドバー Files タブと Cmd+P の「重い」「欲しいファイルが出ない」を
VSCode Quick Open と同じ仕組みに寄せて改善 (#359)。

## workspace-fs (core)
- buildSearchIndex を ripgrep --files ベースに切替。.gitignore /
  .rgignore / .git/info/exclude を自動尊重するようになり、dist/build
  など gitignore されたビルド成果物はデフォルトで除外される
- ripgrep 不在環境向けに fast-glob フォールバックを維持
- DEFAULT_IGNORE_PATTERNS を .git / node_modules の最小限に縮小
- searchFiles に openFilePaths / recentFilePaths を追加。Quick Open
  と同じく開いているファイルは +2000、MRU は +1000、さらに同スコア
  時は recency 順に並べる range boost を実装
- searchFiles に AbortController ベースのキャンセル機構を追加し、
  スコアリングループが中断できるよう yield ポイントを設置
- warmupSearchIndex API を追加し、初回 Cmd+P で ripgrep の初回
  走査待ちが発生しないようにした
- Fuse.js 依存とデッドコード (_collectExactFileSearchMatches 等) を削除

## transport / UI
- tRPC: searchFiles に新パラメータを足し、warmupSearchIndex
  プロシージャを追加 (desktop / host-service)
- useFileSearch / useWorkspaceFileSearch の両方で MRU / include /
  exclude を使えるように feature parity を確保
- useCommandPalette で openFilePaths / recentFilePaths を受け取り
  searchFiles に転送。ダイアログ初回オープン時に warmup を発火
- v2-workspace page.tsx から絶対パスリストを useCommandPalette に
  渡して MRU/open boost を有効化
- FileTreeToolbar の自前 debounce を削除し、hook の 150ms debounce
  に一本化 (二重 debounce を解消)

## tests
- open file boost / MRU tiebreaker / warmup のテストを追加
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

ripgrepバイナリを統合し、ファイル検索にスコア付けの最適化を追加しました。openFilePathsrecentFilePathsscopeIdパラメータを導入し、ファイル検索でアイテムのランキングを制御します。fuse.jsを削除してripgrepベースのファイル列挙に置き換え、インデックスウォーミング機能を実装しました。

Changes

Cohort / File(s) Summary
Ripgrep統合と依存関係
apps/desktop/package.json, package.json, packages/workspace-fs/package.json
ripgrep関連のnpm依存関係(@vscode/ripgrephttps-proxy-agentproxy-from-envyauzl)を追加し、fuse.jsを削除。信頼できる依存関係リストを更新。
Ripgrep実行とパッケージ化
apps/desktop/runtime-dependencies.ts, apps/desktop/src/lib/trpc/routers/workspace-fs-service.ts
ripgrepをランタイム依存関係として設定し、ASAR解凍時の処理に対応。execFileを使用してバンドルされたripgrepバイナリを実行し、ASARパッケージ内の実行パスを動的に解決。
tRPC APIスキーマ拡張
apps/desktop/src/lib/trpc/routers/filesystem/index.ts, packages/host-service/src/trpc/router/filesystem/filesystem.ts, packages/workspace-fs/src/core/service.ts
searchFilesopenFilePathsrecentFilePathsscopeIdパラメータを追加。新しいwarmupSearchIndexミューテーションエンドポイントを実装。
検索UI層の更新
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx, apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/hooks/useWorkspaceFileSearch/useWorkspaceFileSearch.ts, apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/hooks/useFileSearch/useFileSearch.ts
useWorkspaceFileSearchuseFileSearchフックにオプションパラメータを追加し、tRPCクエリペイロードに転送。
CommandPaletteの強化
apps/desktop/src/renderer/screens/main/components/CommandPalette/useCommandPalette.ts
openFilePathsrecentFilePathsのサポートを追加し、パレットオープン時にバックグラウンドでインデックスウォーミングを実行。マルチワークスペース検索にscopeIdを追加。
ファイルツールバーの簡素化
apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeToolbar/FileTreeToolbar.tsx
検索状態管理のデバウンスロジックを削除し、制御直通の検索入力に変更。
検索サービスの実装
packages/workspace-fs/src/host/service.ts, packages/workspace-fs/src/client/index.ts
warmupSearchIndexメソッドを追加し、検索パラメータを拡張。ripgrepベースのファイル列挙とスコア付けロジックを実装。
検索ロジックの根本的な書き直し
packages/workspace-fs/src/search.ts
ripgrepベースのファイル列挙、スコーピング付きキャンセレーション、MRU/オープンファイルブースティング、インデックスウォーミングを実装。fuse.jsの依存関係を削除。
テストカバレッジの拡張
packages/workspace-fs/src/search.test.ts, packages/workspace-fs/src/bun-test.d.ts
ウォーミング、スコア付け、並行実行、ripgrepエラーハンドリングを含む包括的なテストを追加。toBeGreaterThanマッチャーを追加。

Sequence Diagram(s)

sequenceDiagram
    actor User as ユーザー
    participant UI as CommandPalette UI
    participant Hook as useCommandPalette
    participant Search as FilesSearch
    participant Index as SearchIndex
    participant RG as Ripgrep

    User->>UI: パレットを開く
    UI->>Hook: useCommandPalette()
    
    activate Hook
    Hook->>Hook: パレットopen状態を検出
    Hook->>Search: warmupSearchIndex mutation (workspaceId)
    activate Search
    Search->>Index: インデックス構築要求
    activate Index
    Index->>RG: ripgrep実行 (--files)
    RG-->>Index: ファイルリスト返却
    Index->>Index: スコア計算エントリを構築
    Index-->>Search: インデックス準備完了
    deactivate Index
    Search-->>Hook: キャッシュ準備完了
    deactivate Search

    User->>UI: 検索テキスト入力
    UI->>Hook: useFileSearch({query, openFilePaths, recentFilePaths})
    
    activate Hook
    Hook->>Search: searchFiles({query, openFilePaths, recentFilePaths, scopeId})
    activate Search
    Search->>Index: キャッシュからスコア計算開始
    Search->>Search: MRUブースト適用 (recentFilePaths)
    Search->>Search: オープンファイルブースト適用 (openFilePaths)
    Search->>Search: スコアでソート
    Search-->>Hook: 結果返却
    deactivate Search
    Hook-->>UI: 検索結果表示
    deactivate Hook
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 ファイルをぴょんぴょん検索する 🔍
Ripgrepの力で、すばやく見つける
キャッシュでぬくぬくウォーミングアップ
最近のファイルもぴょん!優先順位
検索パレット、ウサギのようにすばやく✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PRタイトルは「feat(desktop): Cmd+P と Files タブの検索ロジックを VSCode 準拠に刷新 (#359)」で、変更の主要目的を明確に説明しており、プルリクエストの核となる改善内容を正確に反映している。
Description check ✅ Passed PR説明は詳細かつ構造化されており、テンプレートの主要セクション(Summary、変更内容、主な変更点、設計メモ、テストプラン)を網羅しているが、公式テンプレートの形式とは異なる独自形式を採用している。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/file-search-vscode-parity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ed95e14dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/workspace-fs/src/search.ts Outdated
Comment thread packages/workspace-fs/src/search.ts Outdated
- ripgrep の `--follow=false` は不正なフラグ (exit 2) で、実際には
  ripgrep 経由の列挙が一度も動かず常に fast-glob にフォールバック
  していた。ripgrep はデフォルトで symlink を follow しないため、
  このフラグ自体を削除して挙動を正しくする
- スコアリングループが同期で、yield ポイントがフラグを見るだけで
  実際には event loop に制御を返していなかった。setImmediate ベース
  の yieldToEventLoop ヘルパーを導入し、SCORE_YIELD_INTERVAL 件ごと
  に本当に macrotask をインターリーブする。後続の searchFiles 呼び出し
  が activeFileSearchControllers 経由で abort できるようになる
- ripgrep 呼び出し時の引数をテストで検証するリグレッションテストを追加

Refs: #365 (Codex review comments)
## Patch event の .gitignore 整合性を回復 (High)
patchSearchIndexesForRoot が watch event からファイルを足す際、新 DEFAULT_IGNORE_PATTERNS (.git/ + node_modules/) しか弾いていなかったため、dist/ や .next/ のビルド成果物が visible index に混入していた。patchIgnoreMatchers を FALLBACK_IGNORE_PATTERNS ベースに戻し、ripgrep の full rebuild と同等以上の保守的な除外をかける。

## searchFiles に scopeId を追加 (High)
activeFileSearchControllers が rootPath のみで keying されていたため、同じ workspace の Cmd+P と Files タブが同時検索すると互いを abort しあっていた。SearchFilesOptions に scopeId を追加し、controllerKey を `${rootPath}::${scopeId}` に分離。useFileSearch は "files-tab"、useCommandPalette は "quick-open" / "quick-open-global" を渡す。

## Cold start もキャンセル可能に (Medium)
getSearchIndex() の完了までは AbortController が効かず、初回 rg 走査中のキーストロークが無駄になっていた。raceWithAbort ヘルパーで caller の signal を getSearchIndex の await に race させ、シェアされた build promise は殺さずに caller だけ短絡させる。

## warmup を毎オープンで発火 (Medium)
workspace ごとに warmedWorkspaceRef で 1 回に固定していたため、SEARCH_INDEX_TTL_MS (30s) 切れ後にダイアログを開くと cold start に戻っていた。ref を撤去し、毎オープンで mutation を発火。backend の inFlightBuilds と TTL により実質的な重複コストは無い。

## リグレッションテスト
- patch event が dist/ 配下のファイルを visible index に入れないこと
- scopeId を分けた同一 rootPath の並列 searchFiles が互いを潰さないこと

Refs: #365 (Codex review comments)
@MocA-Love
Copy link
Copy Markdown
Owner Author

ローカルの Codex (gpt-5.4) に追加レビューを依頼し、以下4件を修正しました。

High

  • Patch event の .gitignore 整合性: patchSearchIndexesForRoot から dist/ / .next/ など gitignore されたビルド成果物が visible index に混入していた。shouldIndexRelativePath が参照する ignore matcher を FALLBACK_IGNORE_PATTERNS ベースに戻し、watch event からの追加も保守的に弾く
  • 同一 rootPath の並列 searchFiles が互いを abort: activeFileSearchControllers が rootPath だけで keying されており、Cmd+P と Files タブが同じ workspace で同時検索すると結果が潰れていた。scopeId パラメータを追加し controller key を分離 (Cmd+P = quick-open, Files タブ = files-tab)

Medium

  • Cold start 中の cancel: 初回 getSearchIndex 完了まで AbortController が効いていなかった。raceWithAbort ヘルパーで caller の signal を await に race させ、共有 build promise は殺さずに呼び出し側だけ短絡
  • warmup の TTL 非対応: workspace ごとに ref で 1 回に固定していたため 30s TTL 切れ後に cold start に戻っていた。ref を撤去して毎オープンで warmup 発火 (inFlight + TTL で実質コストなし)

追加リグレッションテスト

  • dist/ 配下の patch が visible index に混入しないこと
  • scopeId を分けた同一 rootPath の並列 searchFiles が互いを潰さないこと

意図通りとして見送った指摘

  • Files タブでの MRU/open boost: VSCode Explorer にも MRU は無く、本 PR の設計上 Cmd+P のみ boost する方針で合意済み
  • rg 失敗時の fast-glob フォールバック: 64MB buffer で現実的な上限は確保されており、フォールバックは安全側の挙動

これまでの実装は `runRipgrep` がシェルの PATH 上の `rg` を呼んでいたため、
ユーザーが `brew install ripgrep` 等で入れていないと .gitignore 尊重の恩恵
が受けられず、VSCode Quick Open 準拠というこの PR のうたい文句が半分しか
届かない状態だった。

VSCode 本体と同じ `@vscode/ripgrep` パッケージを apps/desktop の依存に
追加し、配布物に ripgrep バイナリ本体 (platform 毎のプリビルド) を同梱する。

- apps/desktop/package.json に `@vscode/ripgrep` を追加
- root package.json の `trustedDependencies` にも追加し、bun install 時に
  postinstall が走ってバイナリがダウンロードされるようにする
- runtime-dependencies.ts で externalizedRuntimeModules に登録し、
  - mainExternalizedDependencies に自動追加 (electron-vite でバンドル
    時に inline せず、node_modules から require させる)
  - packagedNodeModuleCopies で配布物の node_modules に複製
  - asarUnpackGlobs で asar 外に展開 (native binary は asar から exec
    できないため)
  - requiredMaterializedNodeModules でビルド時の同梱検証対象にする
- workspace-fs-service.ts の runRipgrep を `@vscode/ripgrep` の rgPath
  ベースに差し替え。asar パスを asar.unpacked に書き換えるロジックも追加

これによりユーザー環境に rg が無くても Cmd+P / Files タブの .gitignore
尊重検索が動作する。ripgrep のバージョンも VSCode と同じ 15.0.0 に固定
されるので挙動差もなくなる。
以前は rg がどんなエラーを返しても fast-glob にサイレントフォールバック
していたため、`--follow=false` 不正フラグのような実装バグが「動いているが
.gitignore 非準拠」という壊れた状態で隠蔽されていた (最初のレビューで
この挙動が実際に見逃されていた)。

- ENOENT (バイナリ未インストール) の場合だけ fallback 継続
  → テスト環境や、apps/desktop 以外で workspace-fs を使う場合の互換用
- 他のエラー (exit 2, buffer overflow, permission denied など) は throw
  → 実装バグが即表面化する

apps/desktop 本番は @vscode/ripgrep で rg を同梱しているので ENOENT は
発生せず、この分岐は事実上テスト用の safety net として残るだけ。

想定しない rg 失敗が throw になるリグレッションテストを追加。
CI の Build ジョブ (electron-builder) が以下で失敗していた:

  ⨯ Production dependency https-proxy-agent not found for package @vscode/ripgrep
  ⨯ production dependency not found parent=@vscode/ripgrep
    dependency=https-proxy-agent version=^7.0.2

@vscode/ripgrep は `postinstall.js` / `download.js` 用に
`https-proxy-agent` / `yauzl` / `proxy-from-env` を production
dependencies として宣言している。electron-builder の bun 用
node_modules traversal collector はこれらを apps/desktop 側から
辿れないと配布物に含められずエラーになる。

runtime では `lib/index.js` が path.join するだけでこれらは使わないが、
package.json の宣言が正として扱われるため apps/desktop の
dependencies に明示的に追加して walker を満たす。
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/hooks/useWorkspaceFileSearch/useWorkspaceFileSearch.ts (1)

29-45: ⚠️ Potential issue | 🟡 Minor

異なる UI サーフェス間での searchFiles 衝突を防ぐため、scopeId を明示的に指定してください。

Cmd+P は "quick-open"、右サイドバー Files タブは "files-tab" を使用していますが、このコンポーネント(v2 workspace Files ペイン)では scopeId が指定されていません。同一の workspaceId に対して複数の UI サーフェスが同時に検索を実行する場合、backend 側で scopeId をコントローラキーとしてキャッシング/abort 制御を行っているため、scopeId の未指定は他の検索リクエストの abort につながる可能性があります。"v2-files-pane" など固有の値を指定することで、並列検索が互いに干渉しないようになります。

提案される修正
 	const { data: searchResults, isFetching } =
 		workspaceTrpc.filesystem.searchFiles.useQuery(
 			{
 				workspaceId,
 				query: debouncedQuery,
 				limit,
 				includePattern,
 				excludePattern,
 				openFilePaths,
 				recentFilePaths,
+				scopeId: "v2-files-pane",
 			},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilesPane/hooks/useWorkspaceFileSearch/useWorkspaceFileSearch.ts
around lines 29 - 45, The search query for
workspaceTrpc.filesystem.searchFiles.useQuery is missing a scopeId, which can
cause cross-surface aborts; update the query input object passed to
workspaceTrpc.filesystem.searchFiles.useQuery (the one that currently includes
workspaceId, query: debouncedQuery, limit, includePattern, excludePattern,
openFilePaths, recentFilePaths) to include a unique scopeId like "v2-files-pane"
so this Files pane uses its own controller key and won’t interfere with other UI
surfaces.
🧹 Nitpick comments (3)
packages/workspace-fs/src/search.test.ts (1)

361-389: expect(...).rejects.toThrow の使用を推奨。

手動の threw フラグよりも、bun:test の組み込みアサーションを使う方が意図が明確で、将来「例外は出たが別物だった」ケースも検知しやすくなります。

♻️ 提案される修正
-		let threw = false;
-		try {
-			await searchFiles({
-				rootPath,
-				query: "alpha",
-				runRipgrep: async () => {
-					// Simulate the exact shape of an argv-parse error (rg exits 2
-					// when it doesn't understand a flag). Pre-hardening, this
-					// failure silently degraded to fast-glob.
-					const error = new Error(
-						"Command failed: rg: unexpected argument for option '--follow'",
-					) as Error & { code?: number };
-					error.code = 2;
-					throw error;
-				},
-			});
-		} catch {
-			threw = true;
-		}
-
-		expect(threw).toEqual(true);
+		await expect(
+			searchFiles({
+				rootPath,
+				query: "alpha",
+				runRipgrep: async () => {
+					// Simulate the exact shape of an argv-parse error (rg exits 2
+					// when it doesn't understand a flag). Pre-hardening, this
+					// failure silently degraded to fast-glob.
+					const error = new Error(
+						"Command failed: rg: unexpected argument for option '--follow'",
+					) as Error & { code?: number };
+					error.code = 2;
+					throw error;
+				},
+			}),
+		).rejects.toThrow(/--follow/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workspace-fs/src/search.test.ts` around lines 361 - 389, Replace the
manual try/catch + threw flag in the "surfaces unexpected ripgrep failures
instead of silently falling back" test with bun:test's promise rejection
assertion: call searchFiles(...) (with the runRipgrep override that throws the
simulated Error) and assert await expect(searchFiles(...)).rejects.toThrow(...)
so the test fails if the promise resolves or rejects with an unexpected value;
reference the searchFiles call and the runRipgrep override when making the
replacement.
apps/desktop/src/renderer/screens/main/components/CommandPalette/useCommandPalette.ts (2)

211-215: query.trim() の重複計算。

query.trim() が同一式内で 2 回呼ばれています。読みやすさと微小なパフォーマンスのため、上部で一度キャプチャするとよりクリーンです(既に L173 で query.trim()useDebouncedValue に渡しているので、trimmedQuery 変数に束ねるとさらに良いです)。

♻️ 提案される修正
-	const debouncedQuery = useDebouncedValue(query.trim(), 150);
+	const trimmedQuery = query.trim();
+	const debouncedQuery = useDebouncedValue(trimmedQuery, 150);
@@
-	const isFetching =
-		scope === "workspace"
-			? singleSearch.isFetching
-			: multiSearchQueries.some((query) => query.isFetching) ||
-				(query.trim().length > 0 && query.trim() !== debouncedQuery);
+	const isFetching =
+		scope === "workspace"
+			? singleSearch.isFetching
+			: multiSearchQueries.some((q) => q.isFetching) ||
+				(trimmedQuery.length > 0 && trimmedQuery !== debouncedQuery);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/CommandPalette/useCommandPalette.ts`
around lines 211 - 215, isFetching computes query.trim() twice which is
redundant; capture trimmedQuery once (reuse the existing trimmed value passed to
useDebouncedValue or create a const trimmedQuery = query.trim()) and replace
both occurrences in the isFetching expression (the conditional that references
scope, singleSearch.isFetching, multiSearchQueries, query.trim(), and
debouncedQuery) so the logic uses trimmedQuery instead of calling query.trim()
multiple times.

132-156: sentinel 方式のメモ化はクレバーですが、意図を補足するとより堅牢です。

\u0000 を区切り文字として join/split する方式は、配列内容が変わった場合のみ React Query が refetch するという目的を簡潔に達成しています。ただし、絶対パスに \u0000 が含まれることは実質ないものの、将来的に他のデータを扱うよう拡張した際に trap になり得ます。現状は問題ありませんが、可能であれば useDeepCompareMemo 的なユーティリティや、配列の同値比較を内部化した薄いカスタム hook に抽出するとより安全です(チル指摘)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/CommandPalette/useCommandPalette.ts`
around lines 132 - 156, The sentinel join/split using "\u0000" (computed into
openFilePathsKey/recentFilePathsKey and split back into
openFilePathsList/recentFilePathsList via useMemo) works but is fragile; replace
it with a small, well-named utility hook (e.g. useDeepCompareMemo or
useStableArrayKey) that performs a deep-equality comparison of the arrays and
returns a stable value (or the original array) for use in React Query keys.
Locate the current useMemo logic around openFilePathsKey, recentFilePathsKey,
openFilePathsList and recentFilePathsList and extract/replace it with the new
hook so memoization is based on deep array equality rather than a sentinel
join/split; ensure the new hook preserves the same behavior (undefined vs []
handling) for downstream consumers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/workspace-fs/src/search.ts`:
- Around line 540-548: The current processing of ripgrep output in the block
using runRipgrep (with FILE_LISTING_RIPGREP_BUFFER_BYTES and cwd: rootPath)
improperly calls entry.trim(), which strips legitimate spaces in filenames;
change the return path to simply split by "\0" and filter out empty entries
(i.e., replace .split("\0").map(entry => entry.trim()).filter(entry =>
entry.length > 0) with .split("\0").filter(entry => entry.length > 0)) so
NUL-delimited filenames (including spaces) are preserved.

---

Outside diff comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/`$workspaceId/hooks/usePaneRegistry/components/FilesPane/hooks/useWorkspaceFileSearch/useWorkspaceFileSearch.ts:
- Around line 29-45: The search query for
workspaceTrpc.filesystem.searchFiles.useQuery is missing a scopeId, which can
cause cross-surface aborts; update the query input object passed to
workspaceTrpc.filesystem.searchFiles.useQuery (the one that currently includes
workspaceId, query: debouncedQuery, limit, includePattern, excludePattern,
openFilePaths, recentFilePaths) to include a unique scopeId like "v2-files-pane"
so this Files pane uses its own controller key and won’t interfere with other UI
surfaces.

---

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/CommandPalette/useCommandPalette.ts`:
- Around line 211-215: isFetching computes query.trim() twice which is
redundant; capture trimmedQuery once (reuse the existing trimmed value passed to
useDebouncedValue or create a const trimmedQuery = query.trim()) and replace
both occurrences in the isFetching expression (the conditional that references
scope, singleSearch.isFetching, multiSearchQueries, query.trim(), and
debouncedQuery) so the logic uses trimmedQuery instead of calling query.trim()
multiple times.
- Around line 132-156: The sentinel join/split using "\u0000" (computed into
openFilePathsKey/recentFilePathsKey and split back into
openFilePathsList/recentFilePathsList via useMemo) works but is fragile; replace
it with a small, well-named utility hook (e.g. useDeepCompareMemo or
useStableArrayKey) that performs a deep-equality comparison of the arrays and
returns a stable value (or the original array) for use in React Query keys.
Locate the current useMemo logic around openFilePathsKey, recentFilePathsKey,
openFilePathsList and recentFilePathsList and extract/replace it with the new
hook so memoization is based on deep array equality rather than a sentinel
join/split; ensure the new hook preserves the same behavior (undefined vs []
handling) for downstream consumers.

In `@packages/workspace-fs/src/search.test.ts`:
- Around line 361-389: Replace the manual try/catch + threw flag in the
"surfaces unexpected ripgrep failures instead of silently falling back" test
with bun:test's promise rejection assertion: call searchFiles(...) (with the
runRipgrep override that throws the simulated Error) and assert await
expect(searchFiles(...)).rejects.toThrow(...) so the test fails if the promise
resolves or rejects with an unexpected value; reference the searchFiles call and
the runRipgrep override when making the replacement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c7371af8-03e6-42d1-8192-083d166eeee9

📥 Commits

Reviewing files that changed from the base of the PR and between 524336d and f9a98dc.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • apps/desktop/package.json
  • apps/desktop/runtime-dependencies.ts
  • apps/desktop/src/lib/trpc/routers/filesystem/index.ts
  • apps/desktop/src/lib/trpc/routers/workspace-fs-service.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/hooks/useWorkspaceFileSearch/useWorkspaceFileSearch.ts
  • apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx
  • apps/desktop/src/renderer/screens/main/components/CommandPalette/useCommandPalette.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/components/FileTreeToolbar/FileTreeToolbar.tsx
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/hooks/useFileSearch/useFileSearch.ts
  • package.json
  • packages/host-service/src/trpc/router/filesystem/filesystem.ts
  • packages/workspace-fs/package.json
  • packages/workspace-fs/src/bun-test.d.ts
  • packages/workspace-fs/src/client/index.ts
  • packages/workspace-fs/src/core/service.ts
  • packages/workspace-fs/src/host/service.ts
  • packages/workspace-fs/src/search.test.ts
  • packages/workspace-fs/src/search.ts

Comment on lines +540 to +548
try {
const { stdout } = await runRipgrep(args, {
cwd: rootPath,
maxBuffer: FILE_LISTING_RIPGREP_BUFFER_BYTES,
});
return stdout
.split("\0")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

ripgrep --files --null output format does it include trailing newline or only null byte separators

💡 Result:

The ripgrep rg --files --null (or -0) command outputs each file path followed immediately by a NUL byte (\0) separator, with no trailing newline after the final path. Paths are NUL-terminated but not newline-terminated, making the output suitable for tools like xargs -0 that expect NUL-delimited input without relying on newlines. This behavior is explicitly documented in multiple ripgrep man pages: "Whenever a file path is printed, follow it with a NUL byte. This includes printing file paths [...] and when printing a list of matching files such as with [...] --files." The --null flag replaces the default newline separator with NUL for file paths in --files mode. No sources indicate trailing newlines are added; the output uses only NUL bytes as separators, consistent with standard Unix tools like find -print0 or git ls-files -z. For example, issues discussing --null confirm it produces clean NUL-separated paths without extra newlines (e.g., intended for xargs -0 usage). If a trailing newline were present, it would break compatibility with NUL-processing tools, but documentation and usage examples affirm it does not.

Citations:


entry.trim() が NUL 区切り出力の保証を損なっています。

Ripgrep の --files --null は NUL バイト(\0)のみで分区し、末尾に改行を追加しません。したがって split("\0") だけで十分です。しかし entry.trim() は「ファイル名に含まれる正当なスペース」も削除してしまい、POSIX でスペースを含むファイル名の検索がスキップされたり不一致が起きる可能性があります。

.split("\0").filter(entry => entry.length > 0) に簡潔にしてください。

提案する修正
-		return stdout
-			.split("\0")
-			.map((entry) => entry.trim())
-			.filter((entry) => entry.length > 0);
+		return stdout.split("\0").filter((entry) => entry.length > 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const { stdout } = await runRipgrep(args, {
cwd: rootPath,
maxBuffer: FILE_LISTING_RIPGREP_BUFFER_BYTES,
});
return stdout
.split("\0")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
try {
const { stdout } = await runRipgrep(args, {
cwd: rootPath,
maxBuffer: FILE_LISTING_RIPGREP_BUFFER_BYTES,
});
return stdout.split("\0").filter((entry) => entry.length > 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/workspace-fs/src/search.ts` around lines 540 - 548, The current
processing of ripgrep output in the block using runRipgrep (with
FILE_LISTING_RIPGREP_BUFFER_BYTES and cwd: rootPath) improperly calls
entry.trim(), which strips legitimate spaces in filenames; change the return
path to simply split by "\0" and filter out empty entries (i.e., replace
.split("\0").map(entry => entry.trim()).filter(entry => entry.length > 0) with
.split("\0").filter(entry => entry.length > 0)) so NUL-delimited filenames
(including spaces) are preserved.

@MocA-Love MocA-Love merged commit 8e6ffa7 into main Apr 20, 2026
14 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