Skip to content

feat(file-explorer): VS Code-style grouped, ranked, paginated Find results - #8355

Open
heyitaki wants to merge 4 commits into
manaflow-ai:mainfrom
heyitaki:feat/find-grouped-results
Open

heyitaki wants to merge 4 commits into
manaflow-ai:mainfrom
heyitaki:feat/find-grouped-results

Conversation

@heyitaki

@heyitaki heyitaki commented Jul 17, 2026 •

Copy link
Copy Markdown

What and why

Find previously streamed ripgrep matches into a flat table in filesystem walk order and stopped at 500 rows. This PR makes a grouped implementation the only Find path: results grouped by file, files ranked by basename relevance, hits preview-sliced and highlighted, loaded in 100-row pages up to a disclosed 5,000-match cap. Existing behavior — status line, keyboard contract, multi-select, drag-out, context menu, configurable open actions — carries over.

Find sidebar showing grouped, ranked results: per-file groups with hit-count badges, highlighted preview slices, and the Match Case / Code Files Only toggles above the search field.

Design notes

  • Ranking runs per newly displayed page, never over the full buffer — prior rows stay stable, no scroll jumps.
  • FileSearchSnapshot.isSearching now means the backend is genuinely active: early .matches pages render before ripgrep exits, pure .searching frames touch only the status line, and content-revision refreshes defer to the terminal frame.
  • Pagination replaces the 500-row .limited state: 100-row pages, 5,000-match hard cap (SIGTERM at cap), disclosed via totalMatchCount/isTruncated.
  • Typing debounce drops from 200 ms to 50 ms with in-flight process cancellation; displayed rows are never cleared mid-burst.

Performance

The grouped view does strictly more per frame than the flat table it replaces — grouping, ranking, an outline hierarchy, and a 5,000-match buffer vs the old 500-row cap — so interactive costs were profiled and kept in the low milliseconds (Debug/-Onone harness, 5,000-result buffer, Apple M5 Pro): a 100-result page append applies in ~3 ms, a query-change re-render with rows on screen in ~12 ms, and a full 5,000-result cache reseed in ~190 ms (one-time, on Find-tab remount). Ranking and grouping run only at the snapshot boundary, duplicate frames short-circuit at O(1), outline updates are diff-aware, and group expansions coalesce into a single update transaction per apply.

Compatibility and testing

  • Multi-select, drag, the full context menu, Return/double-click/open-shortcut routing, Escape/arrow keyboard behavior, and global font magnification all carry over from the flat table. Match Case / Code Files Only persist via UserDefaults; Find query/results/expansion persist per workspace.
  • App build green; each commit builds standalone, so the series bisects cleanly; pbxproj test-wiring lint passes.
  • 67 XCTest + 57 Swift Testing tests green, with only two pre-existing environment failures that reproduce identically on clean upstream/main. Upstream's debounce, search-field commit, and content-revision sentinel tests pass unmodified.
  • All five new user-facing strings localized (en + ja).

Follow-ups

  • Port the tree-icon cache/reload-storm optimization (fork commit 9f52dada4a) in its own PR.

🤖 Generated with Claude Code

heyitaki and others added 4 commits July 17, 2026 07:50
…nd icon models for Find results

Pure model layer for the grouped Find pipeline: basename-relevance
tiering (stem match > basename contains > body-only), per-file grouping,
match preview slicing with highlight ranges, and cached per-extension
result icons. No behavior change to the app target yet.

Claude-Session: 027daef1-4981-4bba-922c-0738c4fba193
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
…ne view

Replace the flat walk-order results table with a grouped NSOutlineView:
files tiered by basename relevance, hits preview-sliced and highlighted,
loaded in 100-row pages up to a disclosed 5,000-match hard cap (SIGTERM
at cap). Snapshot lifecycle is now accurate on every emit - pure
.searching frames update only the status line while early match pages
render before ripgrep exits, and content-revision refreshes defer to the
terminal frame. Typing debounce drops from 200ms to 50ms with in-flight
process cancellation. Keyboard contract, multi-select, drag-out,
terminal path insertion, context menu, and configurable open actions
carry over to the grouped view.

Claude-Session: 027daef1-4981-4bba-922c-0738c4fba193
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
…space

Store the Find query and last snapshot in FileExplorerStore behind
equality-guarded, non-published setters (streaming frames never
invalidate the SwiftUI workspace tree), reseed the search UI from cache
on remount with settled lifecycle normalization, and clear state when
the workspace changes via beginWorkspace(_:).

Claude-Session: 027daef1-4981-4bba-922c-0738c4fba193
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
… pass

Expanding N newly inserted groups one call at a time makes NSOutlineView
run a full NSTableRowData pass per call (visible-row frame maintenance
plus total-height span recomputation over the row-height index sets).
Wrapping the expand loops in a dedicated beginUpdates/endUpdates
transaction - kept separate from the structural insert batch, which
measures ~2.5x worse when merged - coalesces that work into one pass:

- 100-result page append at a 5,000-result buffer: 7.3ms -> 2.9ms
- query-change re-render with materialized rows: 38ms -> 12ms
- initial 5,000-result apply: 451ms -> 187ms

Medians from interleaved A/B rounds of a Debug/-Onone apply harness,
900x900 viewport, Apple M5 Pro. All 67 focused unit tests pass
unchanged.

Claude-Session: 027daef1-4981-4bba-922c-0738c4fba193
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

@heyitaki is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

File Explorer Find now uses grouped outline results with sticky headers, incremental updates, pagination, hard result limits, configurable case/code filtering, persisted workspace state, and updated context-menu and keyboard integrations.

Changes

File Explorer Find

Layer / File(s) Summary
Search contracts and result preparation
Sources/FileExplorerSearchController.swift, Sources/FileSearch*.swift, Sources/FileExplorerState.swift, Sources/FileExplorerStore.swift, Resources/Localizable.xcstrings
Search snapshots expose pagination and truncation metadata; case-sensitive and code-only options persist; results gain grouping, ranking, preview slicing, and file icons.
Paginated ripgrep pipeline
Sources/FileExplorerSearchController.swift, cmuxTests/FileSearchRipgrepParserTests.swift, cmuxTests/FileExplorerStoreTests.swift
Ripgrep output is buffered into requested pages, capped at 5,000 results, coalesced for UI delivery, and validated through pipeline and controller tests.
Grouped outline results
Sources/FileExplorerSearchResultsView.swift, cmuxTests/FileExplorerSearchResultsViewApplyTests.swift, cmux.xcodeproj/project.pbxproj
The table implementation is replaced by an outline view with grouped file hits, sticky headers, incremental diffs, retained item identities, preview highlighting, context selection, and load-more triggering.
Find controls and workspace integration
Sources/FileExplorerView.swift, Sources/FileExplorerHeaderView.swift, Sources/FileExplorerStore.swift, Sources/KeyboardShortcutContext.swift, Sources/FileExplorerTerminalPathInsertion.swift, cmuxTests/*
Search controls, workspace resets, cached snapshots, keyboard routing, header accessories, and multi-selection context-menu actions are wired to the new results flow.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant FileExplorerContainerView
  participant FileSearchController
  participant FileExplorerSearchResultsView
  User->>FileExplorerContainerView: Enter query and select options
  FileExplorerContainerView->>FileSearchController: Start search
  FileSearchController-->>FileExplorerContainerView: Deliver snapshots
  FileExplorerContainerView->>FileExplorerSearchResultsView: Apply grouped snapshot
  User->>FileExplorerSearchResultsView: Scroll near bottom
  FileExplorerSearchResultsView->>FileSearchController: Request next page
  FileSearchController-->>FileExplorerSearchResultsView: Deliver appended results
Loading

Possibly related PRs

  • manaflow-ai/cmux#3739: Refactors the same File Explorer search controller and pipeline used by this Find results update.

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (10 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error Introduces a shared AppKit image cache via nonisolated(unsafe) plus NSLock, bypassing MainActor ownership for UI state. Make FileSearchResultIcon @MainActor and remove the unsafe static cache/lock; keep AppKit image state main-actor-owned.
Cmux Swift Blocking Runtime ❌ Error FileSearchResultIcon adds a new production NSLock cache around shared NSImage state, which violates the no-manual-locks runtime rule. Move FileSearchResultIcon cache ownership to @MainActor (or another explicit actor) and remove the NSLock/manual synchronization from Sources.
Cmux Cache Substitution Correctness ❌ Error Search view caches lastAppliedIdentity/load-more cursor only by query text; contentRevision/options restarts with same query can reuse stale snapshot state and suppress fresh pagination. Scope apply/load-more caches to search generation (or reset on any Request change), and include generation/contentRevision/options in the short-circuit key.
Cmux Algorithmic Complexity ❌ Error Sources/FileExplorerSearchResultsView.swift:290-303 scans upward from the visible top row on every scroll tick, worst-case O(rows) per event over up to 5k hits. Resolve the sticky-header group directly from the outline hierarchy or cache the current group; avoid per-scroll row scans.
Cmux Swift Package Boundaries ❌ Error Pure, testable search-domain code (FileSearchGrouping, FileSearchPreviewSlice, FileSearchRanking) was added under app-target Sources/ instead of a SwiftPM package. Extract the reusable search core into a small SwiftPM target (e.g. CmuxSearchCore) exposing FileSearchResult first, then move grouping/preview/ranking there; keep AppKit views/glue in the app target.
Cmux User-Facing Error Privacy ❌ Error Visible search failures use raw rg stderr and process.localizedDescription in the status label, exposing upstream details. Replace raw stderr/localizedDescription with generic product copy; keep detailed failure text only in internal logs/telemetry.
Cmux Full Internationalization ❌ Error New fileExplorer.search.* keys only have en/ja, but Localizable.xcstrings supports 20 locales, so the catalog additions are incomplete. Add translated values for the missing locales (ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, zh-Hant) for each new key.
Cmux Architecture Rethink ❌ Error Sources/FileSearchResultIcon.swift adds a process-wide NSLock plus nonisolated(unsafe) shared cache, violating the rule against manual locking instead of one actor/MainActor owner. Move icon caching onto a single @MainActor/actor-owned resolver, remove NSLock and nonisolated(unsafe), and keep any test access in the test target.
Cmux No Test Or Debug Seam In Production Source ❌ Error Production Sources add _resetSymbolCacheForTests, debugPipelineDeliveryCount/debugEnqueuePipelineUpdate, and debugAppliedWorkCount/debugEmptyStateLabelHidden with no prod callers. Move observation into Tests via @testable import or a dedicated debug-only file/folder, and remove these test/debug hooks from production Sources.
Cmux No Ambient Global State ❌ Error New caseless enums (FileSearchResultIcon, SearchResultCellLayout) act as ambient namespaces; FileSearchResultIcon also holds process-wide mutable cache state. Move icon caching into an injectable MainActor-owned helper and replace SearchResultCellLayout with private file-scoped constants or a small owning type.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The description is detailed, but it does not follow the required template and omits sections like Summary, Demo Video, Review Trigger, and Checklist. Reformat the PR description to match the template and add the missing Summary, Testing, Demo Video, Review Trigger, and Checklist sections.
✅ Passed checks (13 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Browser Automation Off-Main ✅ Passed PR changes only File Explorer search files; no edits to TerminalController, ControlCommandExecutionPolicy, or browser socket-worker tests, so this rule isn’t implicated.
Cmux Expensive Synchronous Load ✅ Passed Only change is batching NSOutlineView expansion updates in FileExplorerSearchResultsView; no agent-history/session load, JSONL parsing, or main-thread history load was added or moved.
Cmux No Hacky Sleeps ✅ Passed No new sleep/setTimeout/sleep-shell lines were added in the non-test runtime diff; the remaining timers are structured timeout/backoff logic, not hacky waits.
Cmux Swift Concurrency ✅ Passed The new DispatchQueue/Task usage is limited to Process/pipe I/O and AppKit/MainActor callback boundaries; no new ordinary background app work or Combine app state was introduced.
Cmux Swift @Concurrent ✅ Passed No invalid @concurrent use or missing actor hop found; new ripgrep/SSH/file work is detached or actor-isolated, and UI callers hop before awaiting it.
Cmux Swiftpm Lockfiles ✅ Passed No package-ref or lockfile edits; the deleted CmuxLite .gitignore didn't ignore Package.resolved, and no external pin changes were present.
Cmux Swift Logging ✅ Passed No added runtime logging was found; only existing #if DEBUG NSLog diagnostics remain, and no Logger anti-patterns were introduced.
Cmux Swiftui State Layout ✅ Passed Only legacy ObservableObject/AppKit-bridge state was expanded; no new GeometryReader, lazy-row store refs, or render-time state writes were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No new standalone cmux window/panel/controller/WindowGroup code or close-shortcut identifier changes were added; the PR stays within existing sidebar/search views.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/config/localization files; the commit diff contains no artifact-like paths and the rule allows these file types.
Title check ✅ Passed The title clearly summarizes the main change: grouped, ranked, paginated Find results for file explorer.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR replaces the flat-table ripgrep Find implementation with a grouped, ranked, paginated outline view: results are ranked by basename relevance per page, grouped by file in an NSOutlineView, preview text is sliced and highlighted, and pages load incrementally up to a 5,000-match hard cap. The debounce drops from 200 ms to 50 ms and Find state (query, snapshot, options) is preserved across sidebar-tab switches.

  • New core modules: FileSearchGrouping, FileSearchRanking, FileSearchPreviewSlice, FileSearchResultIcon, and FileExplorerSearchResultsView replace the old FileExplorerSearchResultsTableView/CellView.
  • Controller changes: FileSearchController gains loadMore(), a coalescing pipeline-update slot, FileSearchOptions (match case / code only), and the limited status case is replaced by hasMore/isTruncated/totalMatchCount fields.
  • Two rule violations: five new localization keys ship with only English and Japanese entries (15 locales fall back to English), and three production source files introduce test-only seams (_resetSymbolCacheForTests, debugPipelineDeliveryCount, debugEnqueuePipelineUpdate, debugAppliedWorkCount, debugEmptyStateLabelHidden).

Confidence Score: 3/5

The core search logic is well-structured with thorough tests; two rule violations need fixing before merge.

The new grouping, ranking, preview, and icon modules are clean, well-tested, and correct. The controller coalescing and pagination design is sound. However, _resetSymbolCacheForTests() ships unguarded in production builds, five string keys lack translations for 15 of the 17 supported locales, and three production files carry #if DEBUG test-observability hooks that the codebase policy prohibits.

Sources/FileSearchResultIcon.swift (unguarded test method), Sources/FileExplorerSearchController.swift and Sources/FileExplorerSearchResultsView.swift (#if DEBUG test seams), Resources/Localizable.xcstrings (incomplete locale coverage for all five new keys)

Important Files Changed

Filename Overview
Sources/FileExplorerSearchController.swift Rewrites the search controller with paginated streaming, loadMore() hook, coalesced pipeline update delivery, and FileSearchOptions struct. Adds #if DEBUG test seams (debugPipelineDeliveryCount, debugEnqueuePipelineUpdate) that violate the no-test-seam rule.
Sources/FileExplorerSearchResultsView.swift New file: NSScrollView-based grouped outline view for Find results with sticky group header, diff-aware apply, pagination load-more trigger, and an append-only fast path. Adds #if DEBUG test seams (debugAppliedWorkCount, debugEmptyStateLabelHidden).
Sources/FileSearchResultIcon.swift New file: process-wide SF Symbol icon cache keyed by file extension with language-specific tints. Adds _resetSymbolCacheForTests() — a non-#if DEBUG-guarded test seam that ships in production builds.
Sources/FileSearchGrouping.swift New file: groups flat FileSearchResult arrays by relative path, preserving arrival order, with capacity-pre-sized dictionaries. Straightforward and correct.
Sources/FileSearchRanking.swift New file: stable per-page ranking into three tiers (basename stem match, basename contains, other), with alphabetical secondary sort and an O(n) sorted-hits fast path. Well-documented trade-offs.
Sources/FileSearchPreviewSlice.swift New file: slices ripgrep line previews to a leading-budget window and collects match highlight ranges, with inline whitespace trimming and O(1) isWhitespace using a BMP switch. Correct and well-tested.
Resources/Localizable.xcstrings Adds 4 new keys and renames one key; all new entries carry only English and Japanese translations, leaving 15 of the catalog’s 17 supported locales without translated strings.
Sources/FileExplorerStore.swift Adds findQuery, findSnapshot, currentWorkspaceId, setFindQuery, setFindSnapshot, and beginWorkspace to support cross-tab Find state persistence without publishing.
Sources/FileExplorerView.swift Replaces the flat table search container with FileExplorerSearchResultsView, adds Match Case and Code Only toggle buttons, wires up cache-reseed on tab switch, and reduces debounce from 200ms to 50ms.
Sources/FileExplorerState.swift Adds searchMatchCase and searchCodeOnly as @Published properties backed by UserDefaults, mirroring the pattern used for showHiddenFiles.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as FileExplorerView
    participant Ctrl as FileSearchController
    participant Pipeline as FileSearchOutputPipeline
    participant rg as ripgrep process
    participant View as FileExplorerSearchResultsView

    UI->>Ctrl: search(query, rootPath, options)
    Ctrl->>rg: Process.run() [detached Task]
    rg-->>Pipeline: stdout chunks
    Pipeline-->>Ctrl: FileSearchPipelineUpdate (early emit)
    Ctrl->>Ctrl: enqueuePipelineUpdate (coalescing slot)
    Ctrl->>Ctrl: appendNewlyBufferedToDisplay + rank page
    Ctrl->>View: onSnapshotChanged(snapshot)
    View->>View: apply(snapshot) diff-aware outline update
    Note over View: User scrolls near bottom
    View->>UI: onLoadMoreRequested
    UI->>Ctrl: loadMore()
    Ctrl->>Pipeline: requestNextEmission(target)
    Pipeline-->>Ctrl: update when buffer reaches target
    Ctrl->>View: onSnapshotChanged(snapshot with more rows)
    Note over rg,Pipeline: Hard cap 5000 matches
    rg-->>Pipeline: stdout 5000th match
    Pipeline-->>Ctrl: "shouldStopProcess=true"
    Ctrl->>rg: SIGTERM
    Ctrl->>View: "onSnapshotChanged isTruncated=true"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as FileExplorerView
    participant Ctrl as FileSearchController
    participant Pipeline as FileSearchOutputPipeline
    participant rg as ripgrep process
    participant View as FileExplorerSearchResultsView

    UI->>Ctrl: search(query, rootPath, options)
    Ctrl->>rg: Process.run() [detached Task]
    rg-->>Pipeline: stdout chunks
    Pipeline-->>Ctrl: FileSearchPipelineUpdate (early emit)
    Ctrl->>Ctrl: enqueuePipelineUpdate (coalescing slot)
    Ctrl->>Ctrl: appendNewlyBufferedToDisplay + rank page
    Ctrl->>View: onSnapshotChanged(snapshot)
    View->>View: apply(snapshot) diff-aware outline update
    Note over View: User scrolls near bottom
    View->>UI: onLoadMoreRequested
    UI->>Ctrl: loadMore()
    Ctrl->>Pipeline: requestNextEmission(target)
    Pipeline-->>Ctrl: update when buffer reaches target
    Ctrl->>View: onSnapshotChanged(snapshot with more rows)
    Note over rg,Pipeline: Hard cap 5000 matches
    rg-->>Pipeline: stdout 5000th match
    Pipeline-->>Ctrl: "shouldStopProcess=true"
    Ctrl->>rg: SIGTERM
    Ctrl->>View: "onSnapshotChanged isTruncated=true"
Loading

Reviews (1): Last reviewed commit: "perf(file-explorer): coalesce Find outli..." | Re-trigger Greptile

Comment on lines +98 to +104
/// Test hook, wipes the symbol cache so per-call resolution behavior can
/// be observed independently between cases.
static func _resetSymbolCacheForTests() {
symbolCacheLock.lock()
symbolCache.removeAll(keepingCapacity: true)
symbolCacheLock.unlock()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Test-only seams in production source

Three new test seams violate the no-test-debug-seam-in-production-source rule. The worst offender is _resetSymbolCacheForTests() here: it matches the …ForTests naming pattern, is not compiled out by #if DEBUG, and ships verbatim in production builds. FileExplorerSearchController.swift adds debugPipelineDeliveryCount and debugEnqueuePipelineUpdate under #if DEBUG (lines 568–573), and FileExplorerSearchResultsView.swift adds debugAppliedWorkCount and debugEmptyStateLabelHidden under #if DEBUG (lines 68–78). The canonical fix per cmux PR #6452 is to widen the relevant state from private to internal and read it from the test target via @testable import, rather than bridging it through a named test accessor in production source.

Rule Used: Flag Swift files under a production Sources path (... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@@ -105024,6 +105024,40 @@
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 New string keys missing 15 locale translations

The five added/renamed keys — fileExplorer.search.codeOnly, fileExplorer.search.codeOnly.tooltip, fileExplorer.search.matchCase, fileExplorer.search.matchCase.tooltip, and fileExplorer.search.noResultsFound — carry only en and ja entries. The catalog already contains 17 locales (ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, ru, th, tr, uk), so all three button labels, both tooltips, and the empty-state copy will fall back to English for users in the other 15 locales. Per the full-internationalization rule, every new key must include a translated entry for every locale already present in the touched catalog.

Rule Used: Flag production user-facing text that is not fully... (source)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

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

⚠️ Outside diff range comments (1)
Sources/FileExplorerView.swift (1)

1447-1451: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not expose raw backend failures in the search status.

message can contain error.localizedDescription or ripgrep diagnostics, including local paths and implementation details. Render localized product-level recovery copy and keep sanitized diagnostics internal.

Proposed fix
-        case .failed(let message):
-            return String(
-                format: String(localized: "fileExplorer.search.failed", defaultValue: "Search failed: %@"),
-                message
-            )
+        case .failed:
+            return String(
+                localized: "fileExplorer.search.failed.generic",
+                defaultValue: "Search failed. Try again."
+            )

Add the new key to every supported locale in the touched string catalog.

As per coding guidelines, user-facing errors must not expose raw upstream messages and should provide safe product terms and concrete recovery guidance. <coding_guidelines>

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/FileExplorerView.swift` around lines 1447 - 1451, Update the .failed
case in FileExplorerView to stop interpolating the raw message and instead
return localized, product-level recovery copy with concrete guidance. Keep
backend diagnostics internal, and add the new localization key to every
supported locale in the touched string catalog.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/FileExplorerStoreTests.swift`:
- Around line 840-843: Bound the pagination loop around controller.loadMore() to
at most 50 pages, asserting on each iteration that the snapshot result count
increases; use the test’s asynchronous completion signal or a deadline-bounded
predicate check so stalled hasMore state fails rather than hangs.
- Around line 1257-1287: Update the search status formatter and localization
catalog to use explicit plural keys for the matches message, selecting the
`.one` variant for a count of one and `.other` for all other counts. In the
search status test around the existing totalMatchCount assertions, add coverage
for a single match and verify it uses the singular localized text while
preserving the existing plural assertion.
- Around line 871-873: Replace the single Task.yield() in the snapshot assertion
test with a deadline-bounded wait that polls until
snapshots.last?.totalMatchCount == 5, then retain the existing delivery-count
and snapshot assertions after the predicate succeeds.

In `@cmuxTests/FileSearchPreviewSliceTests.swift`:
- Around line 128-143: Remove XCTest.measure wall-clock benchmarks from
FileSearchPreviewSliceTests.swift lines 128-143, FileSearchRankingTests.swift
lines 108-127, and FileSearchResultIconTests.swift lines 99-128. Preserve each
test’s deterministic slicing, ordering/complexity, and cache-identity
assertions, while moving throughput or performance benchmarking to a separate
benchmark target or suite.

In `@Resources/Localizable.xcstrings`:
- Around line 105112-105145: Update the Japanese values for both
fileExplorer.search.matchCase and fileExplorer.search.matchCase.tooltip to use
大文字と小文字を区別, leaving the English strings unchanged.

In `@Sources/FileExplorerSearchController.swift`:
- Around line 678-684: Update the process-launch failure handling in the search
controller to stop using error.localizedDescription for the user-facing .failed
status. Replace it with localized, product-level recovery text that does not
expose executable paths or POSIX details, while keeping the raw error available
only for internal handling.
- Around line 568-574: Remove the DEBUG-only debugPipelineDeliveryCount property
and debugEnqueuePipelineUpdate method from
Sources/FileExplorerSearchController.swift, replacing their test usage with an
internal coalescer/API or observable test snapshots exposed through `@testable`
import. Update cmuxTests/FileExplorerStoreTests.swift lines 848-890 to exercise
that internal API or the real pipeline behavior, without adding production-only
test seams.
- Around line 810-819: Update finish(generation:update:) to preserve the
hard-cap signal from the completed pipeline update: when pipeline.finish
indicates shouldStopProcess is true, set didHitHardCap before emitting the
settled status. Ensure the resulting snapshot reports isTruncated correctly
while leaving normal completion behavior unchanged.
- Around line 759-765: Update the pagination Task in the load-more flow around
requestNextEmission to revalidate that self.pipeline is still identical to the
captured pipeline after await and before applyPipelineUpdate. Return without
applying the update when the pipeline is no longer authoritative, while
preserving the existing generation check in applyPipelineUpdate.

In `@Sources/FileExplorerSearchResultsView.swift`:
- Around line 1200-1209: Replace the static-only SearchResultCellLayout
namespace with a private immutable layout value owned by the search-result-cell
composition, and update all references to use that shared instance. Preserve the
existing chevron, icon, and hit-text metric values without introducing ambient
top-level state.
- Around line 290-316: The updateStickyHeader method currently scans preceding
outline rows to find the owning SearchResultGroupItem on every scroll event.
Replace the backward row loop with direct outline-hierarchy lookup from topRow,
resolving the top item’s parent group without iterating through the collection,
while preserving the existing stickyGroup and visibility behavior when no group
is found.
- Around line 68-78: Remove the DEBUG-only debugAppliedWorkCount and
debugEmptyStateLabelHidden seams from
Sources/FileExplorerSearchResultsView.swift. In
cmuxTests/FileExplorerSearchResultsViewApplyTests.swift lines 20-108, test the
extracted internal short-circuit policy; at lines 142-157, verify empty-state
behavior through observable UI state rather than an accessor; and at lines
264-282, reuse the policy test or use a genuine performance benchmark instead of
the production counter.
- Around line 61-67: Scope search-result caching and pagination deduplication by
the controller’s authoritative generation: add that generation to
FileSearchSnapshot, AppliedIdentity, and the load-more cursor, and require
generation matches when reusing cached results or rejecting duplicate pages.
Replace query/count-based lifecycle checks with this single generation source so
refreshed or option-changed searches reset correctly and can load their first
page.
- Around line 424-455: Update the pagination identity map and lookups in the
append-groups flow to use each group’s relativePath instead of path. Keep
existingByPath (or rename it consistently) keyed by relativePath, and apply the
same key when inserting newly created SearchResultGroupItem instances so
grouping matches the grouper and equality behavior.

In `@Sources/FileSearchResultIcon.swift`:
- Around line 98-104: Remove _resetSymbolCacheForTests from
Sources/FileSearchResultIcon.swift. In
cmuxTests/FileSearchResultIconTests.swift, stop calling it from setUp and remove
or redesign the reset-specific test using test-target observation or an injected
cache owner; update all three listed sites accordingly.
- Around line 58-96: Update cachedSymbol and its cache state to be owned by
`@MainActor`, removing nonisolated(unsafe), NSLock, and manual synchronization.
Keep the public helpers’ off-main callability by routing cache access through
the existing MainActor UI ownership boundary, while ensuring all NSImage
creation and shared image access occur on MainActor.

---

Outside diff comments:
In `@Sources/FileExplorerView.swift`:
- Around line 1447-1451: Update the .failed case in FileExplorerView to stop
interpolating the raw message and instead return localized, product-level
recovery copy with concrete guidance. Keep backend diagnostics internal, and add
the new localization key to every supported locale in the touched string
catalog.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4090c270-1358-4104-82f7-f83a8ee2cbd5

📥 Commits

Reviewing files that changed from the base of the PR and between b984c9d and 0890fd5.

📒 Files selected for processing (27)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/FileExplorerHeaderView.swift
  • Sources/FileExplorerKeyboardShortcuts.swift
  • Sources/FileExplorerSearchController.swift
  • Sources/FileExplorerSearchResultCellView.swift
  • Sources/FileExplorerSearchResultsTableView.swift
  • Sources/FileExplorerSearchResultsView.swift
  • Sources/FileExplorerState.swift
  • Sources/FileExplorerStore.swift
  • Sources/FileExplorerTerminalPathInsertion.swift
  • Sources/FileExplorerView.swift
  • Sources/FileSearchGrouping.swift
  • Sources/FileSearchPreviewSlice.swift
  • Sources/FileSearchRanking.swift
  • Sources/FileSearchResultIcon.swift
  • Sources/KeyboardShortcutContext.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/AppDelegateFileExplorerShortcutRoutingTests.swift
  • cmuxTests/FileExplorerSearchResultsViewApplyTests.swift
  • cmuxTests/FileExplorerShortcutSettingsTests.swift
  • cmuxTests/FileExplorerStoreTests.swift
  • cmuxTests/FileSearchGroupingTests.swift
  • cmuxTests/FileSearchPreviewSliceTests.swift
  • cmuxTests/FileSearchRankingTests.swift
  • cmuxTests/FileSearchResultIconTests.swift
  • cmuxTests/FileSearchRipgrepParserTests.swift
💤 Files with no reviewable changes (2)
  • Sources/FileExplorerSearchResultCellView.swift
  • Sources/FileExplorerSearchResultsTableView.swift

Comment on lines +840 to +843
while finalSnapshot.hasMore {
controller.loadMore()
finalSnapshot = try #require(snapshots.last)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Bound the pagination loop so regressions fail instead of hanging.

If loadMore() leaves hasMore true without making progress, this loop never terminates. Cap it at the 50 possible pages and assert that each iteration increases the result count.

As per coding guidelines, asynchronous tests must use completion signals or deadline-bounded predicate checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FileExplorerStoreTests.swift` around lines 840 - 843, Bound the
pagination loop around controller.loadMore() to at most 50 pages, asserting on
each iteration that the snapshot result count increases; use the test’s
asynchronous completion signal or a deadline-bounded predicate check so stalled
hasMore state fails rather than hangs.

Source: Coding guidelines

Comment on lines +871 to +873
await Task.yield()
#expect(controller.debugPipelineDeliveryCount == 1)
#expect(snapshots.last?.totalMatchCount == 5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wait for the coalesced snapshot, not one scheduler yield.

Task.yield() does not guarantee that the MainActor drain has executed. Await a deadline-bounded predicate such as snapshots.last?.totalMatchCount == 5 before asserting.

As per coding guidelines, tests must await real completion signals or deadline-bounded predicates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FileExplorerStoreTests.swift` around lines 871 - 873, Replace the
single Task.yield() in the snapshot assertion test with a deadline-bounded wait
that polls until snapshots.last?.totalMatchCount == 5, then retain the existing
delivery-count and snapshot assertions after the predicate succeeds.

Source: Coding guidelines

Comment on lines +1257 to +1287
#expect(statusLabel.stringValue == String(
format: String(localized: "fileExplorer.search.searching", defaultValue: "%d matches, searching"),
0
))
#expect(resultsView.debugAppliedWorkCount == 0)

let results = [Self.searchResult(relativePath: "first.txt")]
searchController.publish(FileSearchSnapshot(
query: "needle",
results: results,
status: .matches,
isSearching: true,
totalMatchCount: 100
))
#expect(statusLabel.stringValue == String(
format: String(localized: "fileExplorer.search.searching", defaultValue: "%d matches, searching"),
100
))
#expect(resultsView.debugAppliedWorkCount == 1)

searchController.publish(FileSearchSnapshot(
query: "needle",
results: results,
status: .matches,
isSearching: false,
totalMatchCount: 650
))
#expect(statusLabel.stringValue == String(
format: String(localized: "fileExplorer.search.matches", defaultValue: "%d matches"),
650
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover singular counts with explicit plural localization keys.

These assertions lock the UI to %d matches; a total of one can render as “1 matches.” Update the status formatter/catalog to use .one and .other, and add a one-match assertion.

Based on learnings, cmux pluralized strings should use explicit .one and .other localization keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FileExplorerStoreTests.swift` around lines 1257 - 1287, Update the
search status formatter and localization catalog to use explicit plural keys for
the matches message, selecting the `.one` variant for a count of one and
`.other` for all other counts. In the search status test around the existing
totalMatchCount assertions, add coverage for a single match and verify it uses
the singular localized text while preserving the existing plural assertion.

Source: Learnings

Comment on lines +128 to +143
/// Perf guardrail, slicing a single visible hit cell must stay fast even
/// with a worst-case long-line preview. The Find sidebar's
/// `refreshVisibleHitCells()` walks every visible row on snapshot apply,
/// so a regression that reintroduced quadratic trim would show up here as
/// a 100×+ slowdown.
func testLongLineSliceIsCheap() {
let leading = String(repeating: " ", count: 200)
let body = String(repeating: "foo bar baz ", count: 25)
let trailing = String(repeating: " ", count: 100)
let preview = leading + body + "needle" + trailing
measure {
for _ in 0..<2000 {
_ = FileSearchPreviewSlicer.slice(preview: preview, query: "needle")
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move wall-clock benchmarks out of correctness tests. These XCTest.measure blocks make the suites dependent on host and CI timing.

  • cmuxTests/FileSearchPreviewSliceTests.swift#L128-L143: retain deterministic slicing assertions and benchmark separately.
  • cmuxTests/FileSearchRankingTests.swift#L108-L127: retain ordering/complexity assertions and benchmark separately.
  • cmuxTests/FileSearchResultIconTests.swift#L99-L128: retain cache identity assertions and benchmark lookup throughput separately.

As per coding guidelines, tests must avoid measured wall-clock dependencies. <coding_guidelines>

📍 Affects 3 files
  • cmuxTests/FileSearchPreviewSliceTests.swift#L128-L143 (this comment)
  • cmuxTests/FileSearchRankingTests.swift#L108-L127
  • cmuxTests/FileSearchResultIconTests.swift#L99-L128
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmuxTests/FileSearchPreviewSliceTests.swift` around lines 128 - 143, Remove
XCTest.measure wall-clock benchmarks from FileSearchPreviewSliceTests.swift
lines 128-143, FileSearchRankingTests.swift lines 108-127, and
FileSearchResultIconTests.swift lines 99-128. Preserve each test’s deterministic
slicing, ordering/complexity, and cache-identity assertions, while moving
throughput or performance benchmarking to a separate benchmark target or suite.

Source: Coding guidelines

Comment on lines +105112 to +105145
"fileExplorer.search.matchCase": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Match case"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "大文字小文字を区別"
}
}
}
},
"fileExplorer.search.matchCase.tooltip": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Match Case"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "大文字小文字を区別"
}
}
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target entries with line numbers.
sed -n '105100,105155p' Resources/Localizable.xcstrings | cat -n

printf '\n--- locales in catalog ---\n'
python3 - <<'PY'
import json
from pathlib import Path
p = Path('Resources/Localizable.xcstrings')
data = json.loads(p.read_text(encoding='utf-8'))
print(sorted(data.get('localizations', {}).keys()))
PY

printf '\n--- search for the same Japanese phrase ---\n'
rg -n '"大文字小文字を区別"|"大文字と小文字を区別"' Resources/Localizable.xcstrings

Repository: manaflow-ai/cmux

Length of output: 1959


Use 大文字と小文字を区別 for both Japanese strings.
大文字小文字を区別 reads awkwardly here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/Localizable.xcstrings` around lines 105112 - 105145, Update the
Japanese values for both fileExplorer.search.matchCase and
fileExplorer.search.matchCase.tooltip to use 大文字と小文字を区別, leaving the English
strings unchanged.

Comment on lines +290 to +316
private func updateStickyHeader() {
let scrollY = contentView.documentVisibleRect.minY
guard scrollY > 0.5 else {
stickyGroup = nil
stickyContainer.isHidden = true
return
}

// Find the topmost group whose row sits at-or-above the visible top.
// We probe a few pixels INTO the visible area so a group header that's
// exactly at the top still counts as "covered" by the sticky.
let probeY = scrollY + 1
let topRow = outlineView.row(at: NSPoint(x: 0, y: probeY))
guard topRow >= 0 else {
stickyGroup = nil
stickyContainer.isHidden = true
return
}
var group: SearchResultGroupItem?
var r = topRow
while r >= 0 {
if let item = outlineView.item(atRow: r) as? SearchResultGroupItem {
group = item
break
}
r -= 1
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Avoid scanning preceding rows on every scroll event.

With one file containing 5,000 hits, this loop can inspect roughly 5,000 rows for every bounds notification. Resolve the top item’s owning group directly from the outline hierarchy instead.

As per path instructions, production hot paths must avoid repeated full-collection scans around 1,000 records.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/FileExplorerSearchResultsView.swift` around lines 290 - 316, The
updateStickyHeader method currently scans preceding outline rows to find the
owning SearchResultGroupItem on every scroll event. Replace the backward row
loop with direct outline-hierarchy lookup from topRow, resolving the top item’s
parent group without iterating through the collection, while preserving the
existing stickyGroup and visibility behavior when no group is found.

Source: Path instructions

Comment on lines +424 to +455
var existingByPath: [String: SearchResultGroupItem] = [:]
existingByPath.reserveCapacity(groupItems.count)
for item in groupItems {
existingByPath[item.group.path] = item
}

var newItems: [SearchResultGroupItem] = []
outlineView.beginUpdates()
for group in appendedGroups {
if let existing = existingByPath[group.path] {
let oldHitCount = existing.hitItems.count
let appendedHitItems = group.hits.map { SearchResultHitItem(hit: $0) }
existing.hitItems.append(contentsOf: appendedHitItems)
existing.group = FileSearchFileGroup(
path: existing.group.path,
relativePath: existing.group.relativePath,
filename: existing.group.filename,
directoryDisplay: existing.group.directoryDisplay,
hits: existing.group.hits + group.hits
)
outlineView.insertItems(
at: IndexSet(integersIn: oldHitCount..<existing.hitItems.count),
inParent: existing,
withAnimation: []
)
refreshVisibleGroupCell(for: existing)
continue
}

let item = SearchResultGroupItem(group: group)
groupItems.append(item)
existingByPath[group.path] = item

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use relativePath consistently as the group identity.

The grouper, incremental diff, and SearchResultGroupItem equality all identify groups by relativePath, but pagination uses absolute path. Differing path representations can split or merge the wrong group.

Proposed fix
-        var existingByPath: [String: SearchResultGroupItem] = [:]
-        existingByPath.reserveCapacity(groupItems.count)
+        var existingByRelativePath: [String: SearchResultGroupItem] = [:]
+        existingByRelativePath.reserveCapacity(groupItems.count)
         for item in groupItems {
-            existingByPath[item.group.path] = item
+            existingByRelativePath[item.group.relativePath] = item
         }

         for group in appendedGroups {
-            if let existing = existingByPath[group.path] {
+            if let existing = existingByRelativePath[group.relativePath] {
                 // ...
             }

             let item = SearchResultGroupItem(group: group)
             groupItems.append(item)
-            existingByPath[group.path] = item
+            existingByRelativePath[group.relativePath] = item

As per path instructions, correctness-critical identity must have one authoritative structured source.

📝 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
var existingByPath: [String: SearchResultGroupItem] = [:]
existingByPath.reserveCapacity(groupItems.count)
for item in groupItems {
existingByPath[item.group.path] = item
}
var newItems: [SearchResultGroupItem] = []
outlineView.beginUpdates()
for group in appendedGroups {
if let existing = existingByPath[group.path] {
let oldHitCount = existing.hitItems.count
let appendedHitItems = group.hits.map { SearchResultHitItem(hit: $0) }
existing.hitItems.append(contentsOf: appendedHitItems)
existing.group = FileSearchFileGroup(
path: existing.group.path,
relativePath: existing.group.relativePath,
filename: existing.group.filename,
directoryDisplay: existing.group.directoryDisplay,
hits: existing.group.hits + group.hits
)
outlineView.insertItems(
at: IndexSet(integersIn: oldHitCount..<existing.hitItems.count),
inParent: existing,
withAnimation: []
)
refreshVisibleGroupCell(for: existing)
continue
}
let item = SearchResultGroupItem(group: group)
groupItems.append(item)
existingByPath[group.path] = item
var existingByRelativePath: [String: SearchResultGroupItem] = [:]
existingByRelativePath.reserveCapacity(groupItems.count)
for item in groupItems {
existingByRelativePath[item.group.relativePath] = item
}
var newItems: [SearchResultGroupItem] = []
outlineView.beginUpdates()
for group in appendedGroups {
if let existing = existingByRelativePath[group.relativePath] {
let oldHitCount = existing.hitItems.count
let appendedHitItems = group.hits.map { SearchResultHitItem(hit: $0) }
existing.hitItems.append(contentsOf: appendedHitItems)
existing.group = FileSearchFileGroup(
path: existing.group.path,
relativePath: existing.group.relativePath,
filename: existing.group.filename,
directoryDisplay: existing.group.directoryDisplay,
hits: existing.group.hits + group.hits
)
outlineView.insertItems(
at: IndexSet(integersIn: oldHitCount..<existing.hitItems.count),
inParent: existing,
withAnimation: []
)
refreshVisibleGroupCell(for: existing)
continue
}
let item = SearchResultGroupItem(group: group)
groupItems.append(item)
existingByRelativePath[group.relativePath] = item
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/FileExplorerSearchResultsView.swift` around lines 424 - 455, Update
the pagination identity map and lookups in the append-groups flow to use each
group’s relativePath instead of path. Keep existingByPath (or rename it
consistently) keyed by relativePath, and apply the same key when inserting newly
created SearchResultGroupItem instances so grouping matches the grouper and
equality behavior.

Source: Path instructions

Comment on lines +1200 to +1209
enum SearchResultCellLayout {
static let chevronX: CGFloat = 8
static let chevronWidth: CGFloat = 12
static let chevronIconGap: CGFloat = 4
static let iconWidth: CGFloat = 16
static var iconX: CGFloat { chevronX + chevronWidth + chevronIconGap }
// hit text sits a couple points to the right of the icon's frame leading so it
// visually aligns with the *glyph* of the SF Symbol (which is centered in the 16pt frame),
// not the frame edge.
static var hitTextX: CGFloat { iconX + 2 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Avoid adding a top-level static-only layout namespace.

SearchResultCellLayout introduces an internal ambient namespace under Sources. Make these metrics a private immutable value owned and shared by the result-cell composition.

As per coding guidelines, production Swift source must avoid static-only namespaces and ambient global behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/FileExplorerSearchResultsView.swift` around lines 1200 - 1209,
Replace the static-only SearchResultCellLayout namespace with a private
immutable layout value owned by the search-result-cell composition, and update
all references to use that shared instance. Preserve the existing chevron, icon,
and hit-text metric values without introducing ambient top-level state.

Source: Coding guidelines

Comment on lines +58 to +96
// process-wide cache for configured symbol images keyed on SF
// Symbol name. Capped at a generous bound (~120 entries cover every name
// in the maps below); evictions are not needed in practice. Reads are
// dominant, sticky-header updates fire on every scroll tick, so we use
// an unfair lock for the rare write path rather than serialize behind a
// queue or actor.
// A synchronous lock keeps the tiny cache critical section ordered across AppKit callers without async hops.
private static let symbolCacheLock = NSLock()
nonisolated(unsafe) private static var symbolCache: [String: NSImage] = [:]
nonisolated(unsafe) private static let symbolConfiguration = NSImage.SymbolConfiguration(
pointSize: 12,
weight: .regular
)

private static func cachedSymbol(named name: String) -> NSImage {
symbolCacheLock.lock()
if let cached = symbolCache[name] {
symbolCacheLock.unlock()
return cached
}
symbolCacheLock.unlock()

let base = NSImage(systemSymbolName: name, accessibilityDescription: nil)
?? NSImage(systemSymbolName: "doc", accessibilityDescription: nil)
?? NSImage()
let configured = base.withSymbolConfiguration(symbolConfiguration) ?? base
configured.isTemplate = true

symbolCacheLock.lock()
// Another thread may have raced ahead; prefer the existing entry so
// every caller observes the same reference.
if let existing = symbolCache[name] {
symbolCacheLock.unlock()
return existing
}
symbolCache[name] = configured
symbolCacheLock.unlock()
return configured
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Keep the AppKit image cache under one @MainActor owner.

The public helpers remain callable off-main while nonisolated(unsafe) bypasses isolation and NSLock only protects the dictionary—not subsequent shared NSImage access. These icons are consumed by MainActor AppKit cells, so isolate the cache/helper there instead of adding manual synchronization.

As per coding guidelines, “Keep SwiftUI or AppKit bridge UI lifecycle under one explicit MainActor owner” and do not introduce manual locks where actor ownership can synchronize the state. <coding_guidelines>

Proposed ownership change
-enum FileSearchResultIcon {
+@MainActor
+enum FileSearchResultIcon {
...
-    private static let symbolCacheLock = NSLock()
-    nonisolated(unsafe) private static var symbolCache: [String: NSImage] = [:]
-    nonisolated(unsafe) private static let symbolConfiguration = NSImage.SymbolConfiguration(
+    private static var symbolCache: [String: NSImage] = [:]
+    private static let symbolConfiguration = NSImage.SymbolConfiguration(
...
-        symbolCacheLock.lock()
         if let cached = symbolCache[name] {
-            symbolCacheLock.unlock()
             return cached
         }
-        symbolCacheLock.unlock()
...
-        symbolCacheLock.lock()
-        if let existing = symbolCache[name] {
-            symbolCacheLock.unlock()
-            return existing
-        }
         symbolCache[name] = configured
-        symbolCacheLock.unlock()
         return configured
📝 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
// process-wide cache for configured symbol images keyed on SF
// Symbol name. Capped at a generous bound (~120 entries cover every name
// in the maps below); evictions are not needed in practice. Reads are
// dominant, sticky-header updates fire on every scroll tick, so we use
// an unfair lock for the rare write path rather than serialize behind a
// queue or actor.
// A synchronous lock keeps the tiny cache critical section ordered across AppKit callers without async hops.
private static let symbolCacheLock = NSLock()
nonisolated(unsafe) private static var symbolCache: [String: NSImage] = [:]
nonisolated(unsafe) private static let symbolConfiguration = NSImage.SymbolConfiguration(
pointSize: 12,
weight: .regular
)
private static func cachedSymbol(named name: String) -> NSImage {
symbolCacheLock.lock()
if let cached = symbolCache[name] {
symbolCacheLock.unlock()
return cached
}
symbolCacheLock.unlock()
let base = NSImage(systemSymbolName: name, accessibilityDescription: nil)
?? NSImage(systemSymbolName: "doc", accessibilityDescription: nil)
?? NSImage()
let configured = base.withSymbolConfiguration(symbolConfiguration) ?? base
configured.isTemplate = true
symbolCacheLock.lock()
// Another thread may have raced ahead; prefer the existing entry so
// every caller observes the same reference.
if let existing = symbolCache[name] {
symbolCacheLock.unlock()
return existing
}
symbolCache[name] = configured
symbolCacheLock.unlock()
return configured
}
`@MainActor`
enum FileSearchResultIcon {
// process-wide cache for configured symbol images keyed on SF
// Symbol name. Capped at a generous bound (~120 entries cover every name
// in the maps below); evictions are not needed in practice. Reads are
// dominant, sticky-header updates fire on every scroll tick, so we keep the
// tiny cache on the main actor instead of synchronizing it separately.
private static var symbolCache: [String: NSImage] = [:]
private static let symbolConfiguration = NSImage.SymbolConfiguration(
pointSize: 12,
weight: .regular
)
private static func cachedSymbol(named name: String) -> NSImage {
if let cached = symbolCache[name] {
return cached
}
let base = NSImage(systemSymbolName: name, accessibilityDescription: nil)
?? NSImage(systemSymbolName: "doc", accessibilityDescription: nil)
?? NSImage()
let configured = base.withSymbolConfiguration(symbolConfiguration) ?? base
configured.isTemplate = true
if let existing = symbolCache[name] {
return existing
}
symbolCache[name] = configured
return configured
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/FileSearchResultIcon.swift` around lines 58 - 96, Update cachedSymbol
and its cache state to be owned by `@MainActor`, removing nonisolated(unsafe),
NSLock, and manual synchronization. Keep the public helpers’ off-main
callability by routing cache access through the existing MainActor UI ownership
boundary, while ensuring all NSImage creation and shared image access occur on
MainActor.

Source: Coding guidelines

Comment on lines +98 to +104
/// Test hook, wipes the symbol cache so per-call resolution behavior can
/// be observed independently between cases.
static func _resetSymbolCacheForTests() {
symbolCacheLock.lock()
symbolCache.removeAll(keepingCapacity: true)
symbolCacheLock.unlock()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove the production cache-reset test seam. The tests currently force a test-only API into runtime source.

  • Sources/FileSearchResultIcon.swift#L98-L104: remove _resetSymbolCacheForTests.
  • cmuxTests/FileSearchResultIconTests.swift#L15-L18: stop resetting production state during setUp.
  • cmuxTests/FileSearchResultIconTests.swift#L92-L96: remove or redesign the reset-specific test using test-target observation or an injected owner.

As per path instructions, production files under Sources/ must not add test-only seams. <path_instructions>

📍 Affects 2 files
  • Sources/FileSearchResultIcon.swift#L98-L104 (this comment)
  • cmuxTests/FileSearchResultIconTests.swift#L15-L18
  • cmuxTests/FileSearchResultIconTests.swift#L92-L96
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/FileSearchResultIcon.swift` around lines 98 - 104, Remove
_resetSymbolCacheForTests from Sources/FileSearchResultIcon.swift. In
cmuxTests/FileSearchResultIconTests.swift, stop calling it from setUp and remove
or redesign the reset-specific test using test-target observation or an injected
cache owner; update all three listed sites accordingly.

Source: Path instructions

@teamleaderleo teamleaderleo added the needs a call Finished and held for a team design or product decision (see #13742 and the gallery in #15427) label Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs a call Finished and held for a team design or product decision (see #13742 and the gallery in #15427)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants