Skip to content

Improve Cmd+P command palette search performance#669

Open
lawrencecchen wants to merge 5 commits intomainfrom
issue-668-cmdp-performance
Open

Improve Cmd+P command palette search performance#669
lawrencecchen wants to merge 5 commits intomainfrom
issue-668-cmdp-performance

Conversation

@lawrencecchen
Copy link
Contributor

@lawrencecchen lawrencecchen commented Feb 28, 2026

Summary

  • Cache search results in @State instead of recomputing the full fuzzy scoring pipeline 3-6x per keystroke
  • Cache base entry lists (commands/switcher entries) while palette is open, refresh only on scope change
  • Add 50ms debounce to coalesce rapid keystrokes
  • Remove .id(commandPaletteQuery) that destroyed the entire LazyVStack row tree on every character typed
  • Debug sync function reads cached results instead of re-running the pipeline 2 extra times
  • Add scripts/populate-debug-workspaces.sh for testing with many workspaces

Test plan

  • Run ./scripts/populate-debug-workspaces.sh /tmp/cmux-debug-cmdp-perf.sock to create 20 workspaces with splits
  • Open Cmd+P, type quickly — results should appear with minimal lag
  • Test both modes: workspace switcher (default) and commands (> prefix)
  • Verify fuzzy matching, scoring, and highlight rendering still work correctly
  • Arrow key navigation and Enter to select still work

Closes #668

Summary by CodeRabbit

  • Performance Improvements

    • Command palette now uses private caching for entries and results for faster, immediate feedback when opened.
  • Bug Fixes

    • Switcher reliably refreshes when workspace titles or underlying data change while the palette is open.
  • New Features

    • Debug menu action to populate test workspaces for easier testing.
  • Tests

    • Added UI and regression tests validating switcher refresh, directory-path indexing, and noise handling in the palette.
Cache results, entries, and debounce keystrokes instead of recomputing
the full fuzzy scoring pipeline 3-6x per keystroke:

- Cache search results in @State instead of recomputing on every access
- Cache base entry lists (commands/switcher entries) while palette is open
- Add 50ms debounce to coalesce rapid keystrokes
- Remove .id(commandPaletteQuery) that destroyed the entire LazyVStack
  row tree on every character typed
- Debug sync function reads cached results instead of re-running pipeline

Also add populate-debug-workspaces.sh script for testing with many
workspaces and splits.

Fixes #668
@vercel
Copy link

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Feb 28, 2026 8:14am
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a private command-palette caching layer and lifecycle handling in ContentView, a debug helper and menu item to populate ~20 test workspaces, and new socket-driven UI tests plus Python regression tests validating switcher refresh and directory-indexing behavior.

Changes

Cohort / File(s) Summary
Command Palette Caching & Lifecycle
Sources/ContentView.swift
Introduces private cache state and fingerprints (cachedCommandPaletteResults, cachedCommandPaletteEntries, cachedCommandPaletteScope, cachedCommandPaletteEntriesFingerprint), methods to compute/refresh entries and fingerprints, immediate recompute on open, cache invalidation on dismiss, and switches UI consumers to cached results. Also adds path/token helpers for search indexing.
Debug Test Workspace Generator
Sources/AppDelegate.swift
Adds @objc func populateDebugSearchWorkspaces(_:) to create ~20 named workspaces, assign palette colors, and add terminal splits for variety.
Debug Menu Integration
Sources/cmuxApp.swift
Adds a Debug menu item "Populate 20 Search Test Workspaces" that invokes the new AppDelegate helper.
UI Tests: Socket Client & Switcher Refresh Suite
cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
Adds a socket/JSON-RPC test client, low-level socket I/O fallbacks, and a new CommandPaletteSwitcherRefreshUITests class exercising palette open, workspace creation/rename, and verifying switcher refresh while palette is open.
End-to-End Regression Tests (Python)
tests_v2/test_command_palette_switcher_refresh_on_workspace_rename.py, tests_v2/test_command_palette_switcher_home_root_noise.py
New scripts that drive the app via socket to assert switcher refresh on rename and to validate noise from home-root paths does not surface workspace rows.
Unit/Repo Tests for Indexing Logic
tests/test_command_palette_switcher_directory_noise_indexing.py
Adds a test that statically inspects ContentView.swift to assert presence of homeRelativePathForSearch, directorySegmentTokensForSearch, and expected directory-tokening behavior (home-relative surface tokens, includeAllSegments flag usage).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ContentView as ContentView (UI)
    participant Cache as CommandPaletteCache
    participant Search as SearchPipeline
    participant Data as WorkspaceStore
    User->>ContentView: open palette / type query
    alt palette open
        ContentView->>Cache: refreshCachedCommandPaletteEntries(scope, fingerprint)
        Cache->>Data: read workspace & surface metadata
        ContentView->>Cache: recomputeCommandPaletteResults()
        Cache->>Search: run search pipeline on cached entries
        Search->>Cache: return results
        Cache->>ContentView: cachedCommandPaletteResults
        ContentView->>User: render results
    else typing (while open)
        ContentView->>Cache: update query -> schedule recompute (debounced/coalesced)
        Note right of Cache: debounce/coalesce inputs
        Cache->>Search: recompute after debounce
        Search->>Cache: results
        Cache->>ContentView: cachedCommandPaletteResults
        ContentView->>User: update render
    end
    User->>ContentView: dismiss palette
    ContentView->>Cache: clear cached entries/results/scope
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I cached the hops and kept them neat,

I prepped twenty burrows, quick on their feet.
Titles changed mid-hop — I reboot my trail,
Results stay snappy, no tumble or fail.
Hooray for fresh burrows and search that won't bail!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly summarizes the main objective: improving command palette search performance through caching and debouncing optimizations.
Linked Issues check ✅ Passed The PR implements all core coding requirements from #668: caches search results and base entries [#668], adds debouncing [#668], removes expensive view identity changes [#668], and includes test scripts to validate performance improvements [#668].
Out of Scope Changes check ✅ Passed All changes directly support the performance goals: ContentView caching optimizations, AppDelegate debug helpers, UI test infrastructure for validation, and regression tests for switcher behavior are all aligned with the stated objectives.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-668-cmdp-performance

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

Copy link

@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: c99224cb9c

ℹ️ 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".

commandPaletteDebounceTask = nil
}
commandPaletteDebounceTask = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: workItem)

Choose a reason for hiding this comment

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

P1 Badge Recompute results before accepting Enter submission

Debouncing search updates by 50ms means cachedCommandPaletteResults can lag behind the latest typed query, so if a user types a character and immediately presses Enter, runSelectedCommandPaletteResult can execute the previously-ranked item instead of the intended match. This is user-visible misexecution (wrong workspace/command) and did not exist when results were recomputed synchronously on each query change.

Useful? React with 👍 / 👎.

Comment on lines 2812 to 2814
if currentScope != cachedCommandPaletteScope {
cachedCommandPaletteScope = currentScope
refreshCachedCommandPaletteEntries()

Choose a reason for hiding this comment

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

P2 Badge Refresh cached entries when switcher data mutates

The cache is only refreshed when the scope changes, so while Cmd+P stays open in switcher mode, workspace/tab additions, removals, or renames (for example via socket commands or other window actions) will not appear until the user changes scope or reopens the palette. This introduces stale and incorrect search results compared with the previous behavior that rebuilt entries from live state each render.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR significantly improves Cmd+P command palette performance through strategic caching and debouncing. The changes eliminate redundant computation by caching search results and base entries in @State, add a 50ms debounce to coalesce rapid keystrokes, and remove the expensive .id(commandPaletteQuery) modifier that was destroying/recreating the entire LazyVStack on every character.

Key improvements:

  • Search pipeline now runs once per debounce window instead of 3-6x per keystroke
  • Base entry lists cached while palette is open, refreshed only on scope changes
  • Debug sync functions read from cache instead of recomputing

Trade-offs:

  • Cache intentionally not invalidated when workspaces are added/removed while palette is open (acceptable for v1)
  • Includes helpful test script to populate workspaces for performance validation

The architecture is clean and the changes are well-isolated to the command palette logic.

Confidence Score: 4/5

  • Safe to merge with one minor style improvement suggested
  • Well-designed performance optimization with clear architectural benefits. The caching strategy is sound, debouncing is appropriately tuned, and removal of the .id() modifier eliminates unnecessary view hierarchy recreation. One style suggestion for weak self capture, but no functional issues. Cache invalidation trade-off (not updating when workspaces change while palette is open) is intentional and acceptable for v1.
  • No files require special attention

Important Files Changed

Filename Overview
Sources/ContentView.swift Refactored command palette search to cache results and entries, added 50ms debounce, removed expensive .id() modifier. Minor: strong self capture in debounce closure.
scripts/populate-debug-workspaces.sh New test script that creates 20 workspaces with varied names and splits for performance testing. Safe and straightforward.

Last reviewed commit: c99224c

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

/// Coalesces rapid keystrokes within a 50ms window.
private func scheduleCommandPaletteRecompute() {
commandPaletteDebounceTask?.cancel()
let workItem = DispatchWorkItem { [self] in
Copy link

Choose a reason for hiding this comment

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

Strong self capture creates temporary retain cycle. Use [weak self]:

Suggested change
let workItem = DispatchWorkItem { [self] in
let workItem = DispatchWorkItem { [weak self] in
self?.recomputeCommandPaletteResults()
self?.commandPaletteDebounceTask = nil
Copy link

@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 (2)
Sources/ContentView.swift (2)

2818-2825: ⚠️ Potential issue | 🟡 Minor

Debug snapshot can stay stale when result count is unchanged.

syncCommandPaletteDebugStateForObservedWindow() is only triggered on count/selection changes, so query changes that keep the same count can leave debug rows outdated.

💡 Proposed fix
     private func recomputeCommandPaletteResults() {
         let entries = cachedCommandPaletteEntries
         let query = commandPaletteQueryForMatching
         let queryIsEmpty = query.isEmpty
@@
         cachedCommandPaletteResults = results
             .sorted { lhs, rhs in
                 if lhs.score != rhs.score { return lhs.score > rhs.score }
                 if lhs.command.rank != rhs.command.rank { return lhs.command.rank < rhs.command.rank }
                 return lhs.command.title.localizedCaseInsensitiveCompare(rhs.command.title) == .orderedAscending
             }
+        syncCommandPaletteDebugStateForObservedWindow()
     }

Also applies to: 3002-3008

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

In `@Sources/ContentView.swift` around lines 2818 - 2825, The debug state can
remain stale because syncCommandPaletteDebugStateForObservedWindow() is only
called when cachedCommandPaletteResults.count changes; update the change
observer to trigger on actual result content changes (not just count) so queries
that preserve count still refresh debug rows—replace or add the .onChange that
currently observes cachedCommandPaletteResults.count with an observer on
cachedCommandPaletteResults (or also observe the query string) and ensure
commandPaletteSelectedResultIndex, updateCommandPaletteScrollTarget(...), and
syncCommandPaletteDebugStateForObservedWindow() are invoked within that handler
(also apply the same change to the similar block referencing lines ~3002-3008).

2681-2683: ⚠️ Potential issue | 🟠 Major

Flush pending debounce before executing Enter selection.

At Line 2682 and Line 4494, Enter can run against stale results if typed immediately after a query change (within the 50ms debounce window), causing the wrong command to execute.

💡 Proposed fix
-                    .onSubmit {
-                        runSelectedCommandPaletteResult(visibleResults: visibleResults)
-                    }
+                    .onSubmit {
+                        runSelectedCommandPaletteResult()
+                    }

-    private func runSelectedCommandPaletteResult(visibleResults: [CommandPaletteSearchResult]? = nil) {
-        let visibleResults = visibleResults ?? cachedCommandPaletteResults
+    private func runSelectedCommandPaletteResult() {
+        // Ensure Enter always executes against the latest query state.
+        commandPaletteDebounceTask?.cancel()
+        commandPaletteDebounceTask = nil
+
+        let currentScope = commandPaletteListScope
+        if currentScope != cachedCommandPaletteScope {
+            cachedCommandPaletteScope = currentScope
+            refreshCachedCommandPaletteEntries()
+        }
+        recomputeCommandPaletteResults()
+
+        let visibleResults = cachedCommandPaletteResults
         guard !visibleResults.isEmpty else {
             NSSound.beep()
             return
         }
         let index = commandPaletteSelectedIndex(resultCount: visibleResults.count)
         runCommandPaletteCommand(visibleResults[index].command)
     }

Also applies to: 4493-4500

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

In `@Sources/ContentView.swift` around lines 2681 - 2683, The Enter handler can
run against stale debounced results; before calling
runSelectedCommandPaletteResult(visibleResults: visibleResults) flush any
pending debounce for the command-palette query/update so the latest results are
used. Concretely: locate the onSubmit handler that calls
runSelectedCommandPaletteResult and, before that call, invoke the debouncer's
flush/performNow/executePending method (or await the pending debounce Task) for
the command palette/query debouncer (e.g., commandPaletteDebouncer or
queryDebouncer used elsewhere) to ensure the latest visibleResults are produced;
apply the same change to the other Enter handler block referenced around lines
4493–4500.
🧹 Nitpick comments (2)
scripts/populate-debug-workspaces.sh (2)

83-93: Clarify split comments for accuracy.

The comments mention "every 2nd workspace" and "3rd split for every 4th workspace," but since created starts at 0, the actual behavior is:

  • Right split on workspaces 0, 2, 4, 6, ... (1st, 3rd, 5th, ...)
  • Down split on workspaces 0, 4, 8, ... (1st, 5th, 9th, ...)

The "3rd split" comment is also slightly misleading since it's adding a second split (down) to workspaces that already have a right split.

📝 Suggested comment clarification
-  # Add a split for every 2nd workspace
+  # Add a right split for odd-indexed workspaces (1st, 3rd, 5th, ...)
   if (( created % 2 == 0 )); then
     cmd split --workspace "$WS_ID" --direction right 2>/dev/null || true
     sleep 0.3
   fi

-  # Add a 3rd split for every 4th workspace
+  # Add an additional down split for every 4th workspace (1st, 5th, 9th, ...)
   if (( created % 4 == 0 )); then
     cmd split --workspace "$WS_ID" --direction down 2>/dev/null || true
     sleep 0.3
   fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/populate-debug-workspaces.sh` around lines 83 - 93, Update the inline
comments around the split creation to accurately describe the behavior: note
that the right split (cmd split --direction right) runs when the loop variable
created is even (created % 2 == 0), i.e., for even-indexed workspaces 0,2,4,...,
and the down split (cmd split --direction down) runs when created % 4 == 0,
i.e., for every 4th workspace starting at 0 (workspaces 0,4,8,...); also replace
"3rd split" wording with something like "add a down split (second split) for
every 4th workspace" so it does not imply a third split, referencing the created
variable and the cmd split invocations.

44-56: Some directories may not exist on all systems.

Paths like $HOME/fun, $HOME/.ssh, /var/log, and /etc may not exist or may require elevated permissions. The cd command will fail silently for non-existent paths, which is fine for a test script, but you might want to either:

  1. Use only universally available directories
  2. Add a fallback (e.g., cd $DIR 2>/dev/null || cd $HOME)
♻️ Optional: Use more reliable directories or add fallback
 # Directories to cd into for varied CWD metadata
 DIRS=(
   "$HOME"
   "/tmp"
-  "$HOME/fun"
+  "$HOME/Desktop"
   "$HOME/.config"
   "/usr/local"
   "$HOME/Downloads"
   "$HOME/Documents"
-  "/var/log"
-  "$HOME/.ssh"
-  "/etc"
+  "$HOME/Library"
+  "$HOME/Pictures"
+  "$HOME/Music"
 )

Or update the send command to handle failures:

-  cmd send --workspace "$WS_ID" "cd $DIR && pwd"
+  cmd send --workspace "$WS_ID" "cd $DIR 2>/dev/null || cd ~ && pwd"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/populate-debug-workspaces.sh` around lines 44 - 56, The DIRS array
contains entries that may not exist or be accessible; when iterating over DIRS
and running cd you should guard against failures by checking
existence/permissions or falling back: update the loop that uses DIRS and the cd
command to either test -d "$DIR" && cd "$DIR" || cd "$HOME" (or continue) or use
cd "$DIR" 2>/dev/null || cd "$HOME"; ensure any subsequent send/operation uses
the resolved CWD so failures don't cause wrong behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/populate-debug-workspaces.sh`:
- Around line 72-74: The SURFACES variable is assigned from the output of cmd
list-panes --workspace "$WS_ID" but never used (SC2034); remove the unused
assignment to eliminate dead code by deleting the line that sets SURFACES=$(cmd
list-panes --workspace "$WS_ID" 2>/dev/null || true), or if you intended to act
on panes implement the missing logic that uses SURFACES (e.g.,
selecting/iterating panes) within the same script where SURFACES is referenced.

---

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 2818-2825: The debug state can remain stale because
syncCommandPaletteDebugStateForObservedWindow() is only called when
cachedCommandPaletteResults.count changes; update the change observer to trigger
on actual result content changes (not just count) so queries that preserve count
still refresh debug rows—replace or add the .onChange that currently observes
cachedCommandPaletteResults.count with an observer on
cachedCommandPaletteResults (or also observe the query string) and ensure
commandPaletteSelectedResultIndex, updateCommandPaletteScrollTarget(...), and
syncCommandPaletteDebugStateForObservedWindow() are invoked within that handler
(also apply the same change to the similar block referencing lines ~3002-3008).
- Around line 2681-2683: The Enter handler can run against stale debounced
results; before calling runSelectedCommandPaletteResult(visibleResults:
visibleResults) flush any pending debounce for the command-palette query/update
so the latest results are used. Concretely: locate the onSubmit handler that
calls runSelectedCommandPaletteResult and, before that call, invoke the
debouncer's flush/performNow/executePending method (or await the pending
debounce Task) for the command palette/query debouncer (e.g.,
commandPaletteDebouncer or queryDebouncer used elsewhere) to ensure the latest
visibleResults are produced; apply the same change to the other Enter handler
block referenced around lines 4493–4500.

---

Nitpick comments:
In `@scripts/populate-debug-workspaces.sh`:
- Around line 83-93: Update the inline comments around the split creation to
accurately describe the behavior: note that the right split (cmd split
--direction right) runs when the loop variable created is even (created % 2 ==
0), i.e., for even-indexed workspaces 0,2,4,..., and the down split (cmd split
--direction down) runs when created % 4 == 0, i.e., for every 4th workspace
starting at 0 (workspaces 0,4,8,...); also replace "3rd split" wording with
something like "add a down split (second split) for every 4th workspace" so it
does not imply a third split, referencing the created variable and the cmd split
invocations.
- Around line 44-56: The DIRS array contains entries that may not exist or be
accessible; when iterating over DIRS and running cd you should guard against
failures by checking existence/permissions or falling back: update the loop that
uses DIRS and the cd command to either test -d "$DIR" && cd "$DIR" || cd "$HOME"
(or continue) or use cd "$DIR" 2>/dev/null || cd "$HOME"; ensure any subsequent
send/operation uses the resolved CWD so failures don't cause wrong behavior.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1392bd1 and c99224c.

📒 Files selected for processing (2)
  • Sources/ContentView.swift
  • scripts/populate-debug-workspaces.sh
Add "Populate 20 Search Test Workspaces" to Debug menu. Creates 20
workspaces with varied names (API Gateway, Auth Service, etc.), colors,
and splits for testing Cmd+P search performance.

Remove the external shell script in favor of the in-app debug menu.
Copy link

@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.

🧹 Nitpick comments (2)
Sources/AppDelegate.swift (2)

4061-4065: Make split seeding deterministic for reproducible perf runs.

Lines 4061 and 4064 use ws.panels.keys.first, which depends on dictionary iteration order and can vary run-to-run.

Proposed change
-            if index % 2 == 0, let panelId = ws.panels.keys.first {
+            if index % 2 == 0, let panelId = ws.focusedPanelId ?? ws.panels.keys.first {
                 ws.newTerminalSplit(from: panelId, orientation: .horizontal)
             }
-            if index % 4 == 0, let panelId = ws.panels.keys.first {
+            if index % 4 == 0, let panelId = ws.focusedPanelId ?? ws.panels.keys.first {
                 ws.newTerminalSplit(from: panelId, orientation: .vertical)
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 4061 - 4065, The current
split-seeding uses a nondeterministic dictionary iteration via
ws.panels.keys.first which makes perf runs vary; change both occurrences to
select a deterministic panel id (for example pick ws.panels.keys.sorted().first
or otherwise consistently choose the smallest/first key) before calling
ws.newTerminalSplit(from:orientation:) so the same panelId is used reproducibly
for horizontal and vertical splits.

4054-4054: Avoid selection churn while bulk-creating debug workspaces.

Line 4054 currently selects each newly created workspace. For seed generation, that extra focus/notification churn is unnecessary overhead.

Proposed change
-            let ws = tabManager.addWorkspace()
+            let ws = tabManager.addWorkspace(select: false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` at line 4054, The code currently calls
TabManager.addWorkspace() which selects each newly created workspace and causes
unnecessary selection churn during bulk seed generation; change the API and
usage so bulk creation can skip selection: add an optional parameter to
TabManager.addWorkspace(select: Bool = true) (or an overload
addWorkspace(selected: Bool)) and update the selection logic inside that method
to only notify/select when select is true, then modify the seed generation code
to call tabManager.addWorkspace(select: false) so new debug workspaces are
created without changing the current selection/notifications.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/AppDelegate.swift`:
- Around line 4061-4065: The current split-seeding uses a nondeterministic
dictionary iteration via ws.panels.keys.first which makes perf runs vary; change
both occurrences to select a deterministic panel id (for example pick
ws.panels.keys.sorted().first or otherwise consistently choose the
smallest/first key) before calling ws.newTerminalSplit(from:orientation:) so the
same panelId is used reproducibly for horizontal and vertical splits.
- Line 4054: The code currently calls TabManager.addWorkspace() which selects
each newly created workspace and causes unnecessary selection churn during bulk
seed generation; change the API and usage so bulk creation can skip selection:
add an optional parameter to TabManager.addWorkspace(select: Bool = true) (or an
overload addWorkspace(selected: Bool)) and update the selection logic inside
that method to only notify/select when select is true, then modify the seed
generation code to call tabManager.addWorkspace(select: false) so new debug
workspaces are created without changing the current selection/notifications.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c99224c and 017e969.

📒 Files selected for processing (2)
  • Sources/AppDelegate.swift
  • Sources/cmuxApp.swift
Copy link

@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: 017e969782

ℹ️ 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 on lines 2811 to 2814
let currentScope = commandPaletteListScope
if currentScope != cachedCommandPaletteScope {
cachedCommandPaletteScope = currentScope
refreshCachedCommandPaletteEntries()

Choose a reason for hiding this comment

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

P2 Badge Refresh command cache when command context changes

This refresh path only rebuilds cachedCommandPaletteEntries when the scope flips between switcher/commands, so in > mode the entry set stays frozen even if commandPaletteContextSnapshot() changes while the palette is open (for example, update availability, unread state, or pin state updates). Because recomputeCommandPaletteResults() searches the cached entries, context-gated commands and labels can become incorrect or missing until users toggle scope or reopen the palette.

Useful? React with 👍 / 👎.

DispatchWorkItem with [self] capture doesn't work in SwiftUI structs —
state mutations from the delayed closure never reach the actual view.
Remove the broken debounce and call recomputeCommandPaletteResults()
synchronously. The main perf wins (result caching, entry caching,
removing .id(commandPaletteQuery)) are preserved.
Copy link

@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: c7344945c5

ℹ️ 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 on lines +2817 to 2823
.onChange(of: cachedCommandPaletteResults.count) { _ in
commandPaletteSelectedResultIndex = commandPaletteSelectedIndex(resultCount: cachedCommandPaletteResults.count)
updateCommandPaletteScrollTarget(resultCount: cachedCommandPaletteResults.count, animated: false)
if let hoveredIndex = commandPaletteHoveredResultIndex, hoveredIndex >= cachedCommandPaletteResults.count {
commandPaletteHoveredResultIndex = nil
}
syncCommandPaletteDebugStateForObservedWindow()

Choose a reason for hiding this comment

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

P2 Badge Sync debug snapshot after every query recompute

The palette now recomputes cached results on commandPaletteQuery changes, but debug synchronization only runs when cachedCommandPaletteResults.count or commandPaletteSelectedResultIndex changes. When a user types a new query that keeps the same result count and leaves selection at index 0, setCommandPaletteSnapshot is not called, so debug.command_palette.results can return stale query/rows from the previous input; this is a regression for debug/automation flows that read command-palette state via the socket API.

Useful? React with 👍 / 👎.

Copy link

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 2817-2820: The onChange is currently observing
cachedCommandPaletteResults.count so updates that mutate items or reorder
without changing count are missed; change the observer to watch the collection
contents (e.g., onChange(of: cachedCommandPaletteResults) or a derived
identity/version such as an array of IDs or a modification token) and call the
same handlers (commandPaletteSelectedResultIndex via
commandPaletteSelectedIndex(...), updateCommandPaletteScrollTarget(...), and the
hovered-index bounds clamp using commandPaletteHoveredResultIndex) so any
content or ordering change triggers the sync; make the same replacement for the
other occurrences referenced (the places around the other two noted locations).
- Around line 2809-2815: The cache is only refreshed when
commandPaletteListScope changes (using commandPaletteListScope vs
cachedCommandPaletteScope), causing stale entries if workspace/tab data mutates
while the palette is open; update the logic to also invalidate or refresh the
cache when underlying live data changes by observing the workspace/tab model or
comparing a data-version/stamp and calling refreshCachedCommandPaletteEntries()
before recomputeCommandPaletteResults(); reference commandPaletteListScope,
cachedCommandPaletteScope, refreshCachedCommandPaletteEntries(), and
recomputeCommandPaletteResults() and add a listener or data-version check to
trigger refreshCachedCommandPaletteEntries() whenever relevant workspace/tab
content updates.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 017e969 and c734494.

📒 Files selected for processing (1)
  • Sources/ContentView.swift
Copy link

@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.

Caution

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

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

2805-2817: ⚠️ Potential issue | 🟠 Major

Missing debounce on the query hot path (and fingerprint work still runs per keystroke).

At Line [2805], the search pipeline still runs immediately for every keypress, and Line [2812] computes a full switcher fingerprint each time. That undercuts the intended coalescing/perf win on large workspace sets.

Suggested fix
@@
-        .onChange(of: commandPaletteQuery) { _ in
+        .onChange(of: commandPaletteQuery) { _ in
             commandPaletteSelectedResultIndex = 0
             commandPaletteHoveredResultIndex = nil
             commandPaletteScrollTargetIndex = nil
             commandPaletteScrollTargetAnchor = nil
@@
-            recomputeCommandPaletteResults()
+            scheduleCommandPaletteResultsRecomputeDebounced()
         }
`@State` private var commandPaletteRecomputeWorkItem: DispatchWorkItem?

private func scheduleCommandPaletteResultsRecomputeDebounced() {
    commandPaletteRecomputeWorkItem?.cancel()
    let work = DispatchWorkItem { [query = commandPaletteQuery] in
        // Optional: guard query unchanged if needed.
        recomputeCommandPaletteResults()
    }
    commandPaletteRecomputeWorkItem = work
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50), execute: work)
}
@@
     private func dismissCommandPalette(restoreFocus: Bool = true) {
+        commandPaletteRecomputeWorkItem?.cancel()
+        commandPaletteRecomputeWorkItem = nil
         let focusTarget = commandPaletteRestoreFocusTarget

Also applies to: 2812-2813

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

In `@Sources/ContentView.swift` around lines 2805 - 2817, Debounce the hot-path
recompute and fingerprint work by adding a DispatchWorkItem stored in
commandPaletteRecomputeWorkItem and invoking recomputeCommandPaletteResults (and
any fingerprint/cache refresh) via a new helper
scheduleCommandPaletteResultsRecomputeDebounced() instead of calling
recomputeCommandPaletteResults() directly inside the onChange(of:
commandPaletteQuery) block; cancel any existing work item, create a new one that
captures the current commandPaletteQuery, and dispatch it with a short
asyncAfter delay (e.g. 50ms) so commandPaletteEntriesFingerprint(for:) and
refreshCachedCommandPaletteEntries(scope:fingerprint:) only run once after
typing pauses. Ensure the onChange handler sets/reset selection state as before
but calls scheduleCommandPaletteResultsRecomputeDebounced() rather than
recomputeCommandPaletteResults() directly.
♻️ Duplicate comments (1)
Sources/ContentView.swift (1)

2819-2826: ⚠️ Potential issue | 🟡 Minor

Count-only observation misses same-size result updates.

At Line [2819], observing cachedCommandPaletteResults.count skips updates when ordering/content changes without count change, so selection/scroll/debug sync can go stale.

Suggested fix
-        .onChange(of: cachedCommandPaletteResults.count) { _ in
+        .onChange(of: cachedCommandPaletteResults.map { "\($0.id):\($0.score)" }) { _ in
             commandPaletteSelectedResultIndex = commandPaletteSelectedIndex(resultCount: cachedCommandPaletteResults.count)
             updateCommandPaletteScrollTarget(resultCount: cachedCommandPaletteResults.count, animated: false)
             if let hoveredIndex = commandPaletteHoveredResultIndex, hoveredIndex >= cachedCommandPaletteResults.count {
                 commandPaletteHoveredResultIndex = nil
             }
             syncCommandPaletteDebugStateForObservedWindow()
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 2819 - 2826, The .onChange is
currently watching cachedCommandPaletteResults.count, which misses updates when
the array's content or order changes without a count change; change the observer
to watch cachedCommandPaletteResults itself (i.e., .onChange(of:
cachedCommandPaletteResults)) so any mutation (content/order) triggers the
handler and then run the same logic that updates
commandPaletteSelectedResultIndex via commandPaletteSelectedIndex(resultCount:),
calls updateCommandPaletteScrollTarget(resultCount:animated:), clears
commandPaletteHoveredResultIndex when out of range, and calls
syncCommandPaletteDebugStateForObservedWindow() to keep selection/scroll/debug
state in sync.
🧹 Nitpick comments (1)
cmuxUITests/MenuKeyEquivalentRoutingUITests.swift (1)

481-559: Consider extracting shared socket logic to reduce duplication.

CommandPaletteV2SocketClient.sendLineDirect() (lines 481-559) and ControlSocketClient.sendLine() (lines 1436-1500) share nearly identical implementations for:

  • UNIX socket creation and connection
  • sockaddr_un setup with manual sun_path memory handling
  • Write loop with partial-write handling
  • Read loop with newline detection

A shared base class or utility function could reduce maintenance burden.

💡 Suggested approach

Extract a private helper function for the common socket send/receive pattern:

private func sendLineToUnixSocket(path: String, line: String) -> (response: String?, error: String?) {
    // Common socket creation, connect, write, read logic
    // Return tuple with response or error description
}

Then both clients can call this helper and add their specific error handling/fallback logic.

Also applies to: 1429-1501

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

In `@cmuxUITests/MenuKeyEquivalentRoutingUITests.swift` around lines 481 - 559,
Extract the duplicated UNIX socket send/receive code into a single private
helper (e.g. sendLineToUnixSocket(path: String, line: String) -> (response:
String?, error: String?)) and replace the bodies of
CommandPaletteV2SocketClient.sendLineDirect and ControlSocketClient.sendLine
with calls to that helper; the helper should encapsulate socket(AF_UNIX,...),
sockaddr_un/sun_path setup and bounds check, connect(), the partial-write loop,
newline-delimited read loop and error string construction so callers only handle
the returned (response, error) tuple and any client-specific logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 2805-2817: Debounce the hot-path recompute and fingerprint work by
adding a DispatchWorkItem stored in commandPaletteRecomputeWorkItem and invoking
recomputeCommandPaletteResults (and any fingerprint/cache refresh) via a new
helper scheduleCommandPaletteResultsRecomputeDebounced() instead of calling
recomputeCommandPaletteResults() directly inside the onChange(of:
commandPaletteQuery) block; cancel any existing work item, create a new one that
captures the current commandPaletteQuery, and dispatch it with a short
asyncAfter delay (e.g. 50ms) so commandPaletteEntriesFingerprint(for:) and
refreshCachedCommandPaletteEntries(scope:fingerprint:) only run once after
typing pauses. Ensure the onChange handler sets/reset selection state as before
but calls scheduleCommandPaletteResultsRecomputeDebounced() rather than
recomputeCommandPaletteResults() directly.

---

Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 2819-2826: The .onChange is currently watching
cachedCommandPaletteResults.count, which misses updates when the array's content
or order changes without a count change; change the observer to watch
cachedCommandPaletteResults itself (i.e., .onChange(of:
cachedCommandPaletteResults)) so any mutation (content/order) triggers the
handler and then run the same logic that updates
commandPaletteSelectedResultIndex via commandPaletteSelectedIndex(resultCount:),
calls updateCommandPaletteScrollTarget(resultCount:animated:), clears
commandPaletteHoveredResultIndex when out of range, and calls
syncCommandPaletteDebugStateForObservedWindow() to keep selection/scroll/debug
state in sync.

---

Nitpick comments:
In `@cmuxUITests/MenuKeyEquivalentRoutingUITests.swift`:
- Around line 481-559: Extract the duplicated UNIX socket send/receive code into
a single private helper (e.g. sendLineToUnixSocket(path: String, line: String)
-> (response: String?, error: String?)) and replace the bodies of
CommandPaletteV2SocketClient.sendLineDirect and ControlSocketClient.sendLine
with calls to that helper; the helper should encapsulate socket(AF_UNIX,...),
sockaddr_un/sun_path setup and bounds check, connect(), the partial-write loop,
newline-delimited read loop and error string construction so callers only handle
the returned (response, error) tuple and any client-specific logic.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c734494 and 0967e5a.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
  • tests_v2/test_command_palette_switcher_refresh_on_workspace_rename.py
Copy link

@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

♻️ Duplicate comments (2)
Sources/ContentView.swift (2)

2810-2816: ⚠️ Potential issue | 🟠 Major

Cache invalidation is still tied to query edits, and fingerprinting now sits in the keystroke hot path.

Line [2812] computes a deep switcher fingerprint on every keypress (Lines [2992-3055]). That reintroduces heavy work during typing, and cache refresh still won’t happen until query/scope changes.

💡 Suggested fix
@@
-            let currentFingerprint = commandPaletteEntriesFingerprint(for: currentScope)
-            if currentScope != cachedCommandPaletteScope || currentFingerprint != cachedCommandPaletteEntriesFingerprint {
-                cachedCommandPaletteScope = currentScope
-                refreshCachedCommandPaletteEntries(scope: currentScope, fingerprint: currentFingerprint)
-            }
+            if currentScope != cachedCommandPaletteScope {
+                cachedCommandPaletteScope = currentScope
+                refreshCachedCommandPaletteEntries(scope: currentScope)
+            }
             recomputeCommandPaletteResults()
@@
         view = AnyView(view.onReceive(tabManager.$tabs) { tabs in
             ...
+            if isCommandPalettePresented,
+               case .commands = commandPaletteMode,
+               commandPaletteListScope == .switcher {
+                refreshCachedCommandPaletteEntries(scope: .switcher)
+                recomputeCommandPaletteResults()
+            }
             ...
         })

Also applies to: 2992-3055

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

In `@Sources/ContentView.swift` around lines 2810 - 2816, The code computes
commandPaletteEntriesFingerprint(currentScope) on every keystroke (via
commandPaletteListScope/currentFingerprint) which is expensive; change the logic
so fingerprinting is not on the hot keystroke path—only compute the fingerprint
when the inputs that affect entries actually change (e.g., scope or query), or
debounce/throttle the fingerprint computation; update the condition that
compares cachedCommandPaletteScope and cachedCommandPaletteEntriesFingerprint to
use a cachedLastQuery/cachedInputs sentinel and call
refreshCachedCommandPaletteEntries(scope:fingerprint:) only after computing the
fingerprint from the new inputs outside the immediate keystroke handler (or via
a short debounce), keeping functions commandPaletteEntriesFingerprint,
cachedCommandPaletteScope, cachedCommandPaletteEntriesFingerprint and
refreshCachedCommandPaletteEntries as the integration points.

2819-2826: ⚠️ Potential issue | 🟡 Minor

Observing only result count misses same-size result updates.

At Line [2819], updates that reorder/mutate results with unchanged count won’t trigger this block, so debug snapshot sync can lag behind visible content.

💡 Suggested fix
@@
     private func recomputeCommandPaletteResults() {
         ...
         cachedCommandPaletteResults = results
             .sorted { lhs, rhs in
                 if lhs.score != rhs.score { return lhs.score > rhs.score }
                 if lhs.command.rank != rhs.command.rank { return lhs.command.rank < rhs.command.rank }
                 return lhs.command.title.localizedCaseInsensitiveCompare(rhs.command.title) == .orderedAscending
             }
+        syncCommandPaletteDebugStateForObservedWindow()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ContentView.swift` around lines 2819 - 2826, The current onChange
observes cachedCommandPaletteResults.count so reorders or content changes that
keep the same count won't trigger sync; change the observer to watch the
collection itself (replace .onChange(of: cachedCommandPaletteResults.count) with
.onChange(of: cachedCommandPaletteResults)) so
commandPaletteSelectedResultIndex, updateCommandPaletteScrollTarget,
commandPaletteHoveredResultIndex cleanup, and
syncCommandPaletteDebugStateForObservedWindow run on any mutation; if the
element type isn't Equatable, observe a stable identifier list instead (e.g.,
.onChange(of: cachedCommandPaletteResults.map { $0.id })).
🧹 Nitpick comments (3)
tests_v2/test_command_palette_switcher_home_root_noise.py (1)

73-73: Prefer state-based waits over fixed sleeps.

The fixed time.sleep(0.2) calls can cause intermittent failures on slower CI machines. Replace them with _wait_until(...) against observable state transitions where possible.

Also applies to: 80-80, 84-84, 90-90

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

In `@tests_v2/test_command_palette_switcher_home_root_noise.py` at line 73,
Replace the brittle fixed delays (the calls to time.sleep(0.2)) with state-based
waits using the test helper _wait_until(...) that polls for an observable
condition; locate each occurrence of time.sleep in this test (the calls at the
shown spot and the other occurrences around lines referenced: the ones at 80,
84, 90) and change them to calls like _wait_until(lambda: <observable state is
reached>, timeout=...) where <observable state is a UI element, flag, or mock
invocation you can assert (e.g., the command palette being open/closed, a
specific DOM node present, or an expected state value). Ensure you pick a
deterministic predicate that reflects the transition you were previously waiting
for and set a reasonable timeout to avoid flakiness.
tests/test_command_palette_switcher_directory_noise_indexing.py (2)

11-19: Fallback to cwd() may produce misleading failures.

If git rev-parse fails (e.g., running outside a git repo or git not installed), falling back to Path.cwd() could cause the test to fail with a confusing "Missing expected file" error rather than indicating the actual problem.

Consider failing early with a clearer message when the git command fails:

Suggested improvement
 def get_repo_root() -> Path:
     result = subprocess.run(
         ["git", "rev-parse", "--show-toplevel"],
         capture_output=True,
         text=True,
     )
-    if result.returncode == 0:
-        return Path(result.stdout.strip())
-    return Path.cwd()
+    if result.returncode != 0:
+        raise RuntimeError("Failed to determine repository root; ensure git is installed and test is run from within the repo")
+    return Path(result.stdout.strip())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_command_palette_switcher_directory_noise_indexing.py` around lines
11 - 19, get_repo_root currently falls back to Path.cwd() when git rev-parse
fails, which hides the real error; change get_repo_root to raise a clear
exception instead of returning cwd: when subprocess.run returns nonzero, include
result.stderr (or at least returncode) in the raised RuntimeError (or custom
exception) so callers see that git failed (e.g., "git rev-parse failed: exit
{returncode}: {stderr}"); keep the successful path (return
Path(result.stdout.strip())) and only use an exception for the failure path so
tests fail fast with a clear message.

62-66: Function body extraction depends on source file ordering.

The regex pattern relies on homeRelativePathForSearch immediately following directoryTokensForSearch in the source file. If another function is added between them or they are reordered, this test will fail even if the actual implementation is correct.

Consider using a less order-dependent delimiter (e.g., matching balanced braces) or documenting this fragility with a comment so future maintainers understand the constraint.

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

In `@tests/test_command_palette_switcher_directory_noise_indexing.py` around lines
62 - 66, The test's regex depends on `homeRelativePathForSearch` immediately
following `directoryTokensForSearch`, which is brittle; update the extraction in
`fn_match` to not rely on file ordering by matching the
`directoryTokensForSearch` function body using balanced-brace logic or a regex
that captures up to the matching closing brace (e.g., implement a small parser
that finds the opening '{' after `private static func directoryTokensForSearch`
and walks characters counting braces until the matching '}'), or alternatively
add a clear comment near the `fn_match` code documenting this ordering
assumption and why it must be preserved; refer to the `directoryTokensForSearch`
and `homeRelativePathForSearch` symbols to locate the relevant test code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests_v2/test_command_palette_switcher_home_root_noise.py`:
- Around line 70-129: The test needs a try/finally so cleanup always runs: after
creating workspace_id (in main) wrap the subsequent assertions and interactions
(everything from _open_switcher(client, window_id) through the checks that may
raise cmuxError) in a try block and move the palette/workspace teardown into a
finally block that calls _set_palette_visible(client, window_id, False) and
client.close_workspace(workspace_id) (guarding that workspace_id is truthy),
ensuring cleanup runs even if an assertion raises; keep the cmux(...) context
and print/return outside the try/finally.

---

Duplicate comments:
In `@Sources/ContentView.swift`:
- Around line 2810-2816: The code computes
commandPaletteEntriesFingerprint(currentScope) on every keystroke (via
commandPaletteListScope/currentFingerprint) which is expensive; change the logic
so fingerprinting is not on the hot keystroke path—only compute the fingerprint
when the inputs that affect entries actually change (e.g., scope or query), or
debounce/throttle the fingerprint computation; update the condition that
compares cachedCommandPaletteScope and cachedCommandPaletteEntriesFingerprint to
use a cachedLastQuery/cachedInputs sentinel and call
refreshCachedCommandPaletteEntries(scope:fingerprint:) only after computing the
fingerprint from the new inputs outside the immediate keystroke handler (or via
a short debounce), keeping functions commandPaletteEntriesFingerprint,
cachedCommandPaletteScope, cachedCommandPaletteEntriesFingerprint and
refreshCachedCommandPaletteEntries as the integration points.
- Around line 2819-2826: The current onChange observes
cachedCommandPaletteResults.count so reorders or content changes that keep the
same count won't trigger sync; change the observer to watch the collection
itself (replace .onChange(of: cachedCommandPaletteResults.count) with
.onChange(of: cachedCommandPaletteResults)) so
commandPaletteSelectedResultIndex, updateCommandPaletteScrollTarget,
commandPaletteHoveredResultIndex cleanup, and
syncCommandPaletteDebugStateForObservedWindow run on any mutation; if the
element type isn't Equatable, observe a stable identifier list instead (e.g.,
.onChange(of: cachedCommandPaletteResults.map { $0.id })).

---

Nitpick comments:
In `@tests_v2/test_command_palette_switcher_home_root_noise.py`:
- Line 73: Replace the brittle fixed delays (the calls to time.sleep(0.2)) with
state-based waits using the test helper _wait_until(...) that polls for an
observable condition; locate each occurrence of time.sleep in this test (the
calls at the shown spot and the other occurrences around lines referenced: the
ones at 80, 84, 90) and change them to calls like _wait_until(lambda:
<observable state is reached>, timeout=...) where <observable state is a UI
element, flag, or mock invocation you can assert (e.g., the command palette
being open/closed, a specific DOM node present, or an expected state value).
Ensure you pick a deterministic predicate that reflects the transition you were
previously waiting for and set a reasonable timeout to avoid flakiness.

In `@tests/test_command_palette_switcher_directory_noise_indexing.py`:
- Around line 11-19: get_repo_root currently falls back to Path.cwd() when git
rev-parse fails, which hides the real error; change get_repo_root to raise a
clear exception instead of returning cwd: when subprocess.run returns nonzero,
include result.stderr (or at least returncode) in the raised RuntimeError (or
custom exception) so callers see that git failed (e.g., "git rev-parse failed:
exit {returncode}: {stderr}"); keep the successful path (return
Path(result.stdout.strip())) and only use an exception for the failure path so
tests fail fast with a clear message.
- Around line 62-66: The test's regex depends on `homeRelativePathForSearch`
immediately following `directoryTokensForSearch`, which is brittle; update the
extraction in `fn_match` to not rely on file ordering by matching the
`directoryTokensForSearch` function body using balanced-brace logic or a regex
that captures up to the matching closing brace (e.g., implement a small parser
that finds the opening '{' after `private static func directoryTokensForSearch`
and walks characters counting braces until the matching '}'), or alternatively
add a clear comment near the `fn_match` code documenting this ordering
assumption and why it must be preserved; refer to the `directoryTokensForSearch`
and `homeRelativePathForSearch` symbols to locate the relevant test code.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0967e5a and 4e92c11.

📒 Files selected for processing (3)
  • Sources/ContentView.swift
  • tests/test_command_palette_switcher_directory_noise_indexing.py
  • tests_v2/test_command_palette_switcher_home_root_noise.py
Comment on lines +70 to +129
def main() -> int:
with cmux(SOCKET_PATH) as client:
client.activate_app()
time.sleep(0.2)

window_id = client.current_window()
for row in client.list_windows():
other_id = str(row.get("id") or "")
if other_id and other_id != window_id:
client.close_window(other_id)
time.sleep(0.2)

client.focus_window(window_id)
client.activate_app()
time.sleep(0.2)

workspace_id = client.new_workspace(window_id=window_id)
client.select_workspace(workspace_id)
token = f"switcher-noise-{int(time.time() * 1000)}"
client.rename_workspace(token, workspace=workspace_id)
time.sleep(0.2)

_open_switcher(client, window_id)

client.simulate_type(token)
_wait_until(
lambda: token in str(_palette_results(client, window_id).get("query") or "").strip().lower(),
message="switcher query did not update to workspace token",
)

workspace_command_id = f"switcher.workspace.{workspace_id.lower()}"
baseline_rows = (_palette_results(client, window_id, limit=80).get("results") or [])
baseline_ids = [str((row or {}).get("command_id") or "") for row in baseline_rows]
if workspace_command_id not in baseline_ids:
raise cmuxError(
"setup failed: workspace row missing for workspace-token query; "
f"expected={workspace_command_id!r} rows={baseline_rows}"
)

noise_query = f"{_home_root_query_token()} {token}"
client.simulate_shortcut("cmd+a")
client.simulate_type(noise_query)
_wait_until(
lambda: noise_query in str(_palette_results(client, window_id).get("query") or "").strip().lower(),
message="switcher query did not update to home-root-noise query",
)

rows = (_palette_results(client, window_id, limit=80).get("results") or [])
matched_ids = [str((row or {}).get("command_id") or "") for row in rows]
if workspace_command_id in matched_ids:
raise cmuxError(
"workspace row should not match home-root noise token combined with workspace token; "
f"query={noise_query!r} unexpected={workspace_command_id!r} rows={rows}"
)

_set_palette_visible(client, window_id, False)
client.close_workspace(workspace_id)

print("PASS: switcher ignores home-root path noise for workspace search matching")
return 0
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use try/finally so test cleanup always runs.

If any check fails before Line 125, workspace/palette cleanup is skipped, which can contaminate later tests on the same socket/session.

Proposed fix
 def main() -> int:
     with cmux(SOCKET_PATH) as client:
         client.activate_app()
         time.sleep(0.2)
@@
-        workspace_id = client.new_workspace(window_id=window_id)
-        client.select_workspace(workspace_id)
-        token = f"switcher-noise-{int(time.time() * 1000)}"
-        client.rename_workspace(token, workspace=workspace_id)
-        time.sleep(0.2)
-
-        _open_switcher(client, window_id)
+        workspace_id = client.new_workspace(window_id=window_id)
+        try:
+            client.select_workspace(workspace_id)
+            token = f"switcher-noise-{int(time.time() * 1000)}"
+            client.rename_workspace(token, workspace=workspace_id)
+            time.sleep(0.2)
+
+            _open_switcher(client, window_id)
@@
-        _set_palette_visible(client, window_id, False)
-        client.close_workspace(workspace_id)
+        finally:
+            try:
+                _set_palette_visible(client, window_id, False)
+            except cmuxError:
+                pass
+            try:
+                client.close_workspace(workspace_id)
+            except cmuxError:
+                pass
🧰 Tools
🪛 Ruff (0.15.2)

[warning] 104-107: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 120-123: Avoid specifying long messages outside the exception class

(TRY003)

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

In `@tests_v2/test_command_palette_switcher_home_root_noise.py` around lines 70 -
129, The test needs a try/finally so cleanup always runs: after creating
workspace_id (in main) wrap the subsequent assertions and interactions
(everything from _open_switcher(client, window_id) through the checks that may
raise cmuxError) in a try block and move the palette/workspace teardown into a
finally block that calls _set_palette_visible(client, window_id, False) and
client.close_workspace(workspace_id) (guarding that workspace_id is truthy),
ensuring cleanup runs even if an assertion raises; keep the cmux(...) context
and print/return outside the try/finally.
Copy link

@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: 4e92c11e79

ℹ️ 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 on lines 2807 to 2809
commandPaletteHoveredResultIndex = nil
commandPaletteScrollTargetIndex = nil
commandPaletteScrollTargetAnchor = nil

Choose a reason for hiding this comment

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

P2 Badge Re-anchor list scroll when query resets selection to index 0

When the query changes, this block forces commandPaletteSelectedResultIndex = 0 and clears the scroll target, but it does not call updateCommandPaletteScrollTarget. The list only re-anchors later if either the selected index changes (it may already be 0) or the result count changes. After removing .id(commandPaletteQuery), a user can scroll the list away from the top, type a query that keeps the same count, and end up with selection still at row 0 but off-screen; pressing Enter then runs an unseen result, which is a regression in command selection behavior.

Useful? React with 👍 / 👎.

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

Labels

None yet

1 participant