Improve Cmd+P command palette search performance#669
Improve Cmd+P command palette search performance#669lawrencecchen wants to merge 5 commits intomainfrom
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a private command-palette caching layer and lifecycle handling in Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 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".
Sources/ContentView.swift
Outdated
| commandPaletteDebounceTask = nil | ||
| } | ||
| commandPaletteDebounceTask = workItem | ||
| DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: workItem) |
There was a problem hiding this comment.
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 👍 / 👎.
Sources/ContentView.swift
Outdated
| if currentScope != cachedCommandPaletteScope { | ||
| cachedCommandPaletteScope = currentScope | ||
| refreshCachedCommandPaletteEntries() |
There was a problem hiding this comment.
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 SummaryThis 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 Key improvements:
Trade-offs:
The architecture is clean and the changes are well-isolated to the command palette logic. Confidence Score: 4/5
Important Files Changed
Last reviewed commit: c99224c |
Sources/ContentView.swift
Outdated
| /// Coalesces rapid keystrokes within a 50ms window. | ||
| private func scheduleCommandPaletteRecompute() { | ||
| commandPaletteDebounceTask?.cancel() | ||
| let workItem = DispatchWorkItem { [self] in |
There was a problem hiding this comment.
Strong self capture creates temporary retain cycle. Use [weak self]:
| let workItem = DispatchWorkItem { [self] in | |
| let workItem = DispatchWorkItem { [weak self] in | |
| self?.recomputeCommandPaletteResults() | |
| self?.commandPaletteDebounceTask = nil |
There was a problem hiding this comment.
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 | 🟡 MinorDebug 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 | 🟠 MajorFlush 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
createdstarts 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/etcmay not exist or may require elevated permissions. Thecdcommand will fail silently for non-existent paths, which is fine for a test script, but you might want to either:
- Use only universally available directories
- 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.
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.
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
💡 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".
Sources/ContentView.swift
Outdated
| let currentScope = commandPaletteListScope | ||
| if currentScope != cachedCommandPaletteScope { | ||
| cachedCommandPaletteScope = currentScope | ||
| refreshCachedCommandPaletteEntries() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| .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() |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | 🟠 MajorMissing 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 = commandPaletteRestoreFocusTargetAlso 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 | 🟡 MinorCount-only observation misses same-size result updates.
At Line [2819], observing
cachedCommandPaletteResults.countskips 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) andControlSocketClient.sendLine()(lines 1436-1500) share nearly identical implementations for:
- UNIX socket creation and connection
sockaddr_unsetup with manualsun_pathmemory 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
📒 Files selected for processing (3)
Sources/ContentView.swiftcmuxUITests/MenuKeyEquivalentRoutingUITests.swifttests_v2/test_command_palette_switcher_refresh_on_workspace_rename.py
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
Sources/ContentView.swift (2)
2810-2816:⚠️ Potential issue | 🟠 MajorCache 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 | 🟡 MinorObserving 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 tocwd()may produce misleading failures.If
git rev-parsefails (e.g., running outside a git repo or git not installed), falling back toPath.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
homeRelativePathForSearchimmediately followingdirectoryTokensForSearchin 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
📒 Files selected for processing (3)
Sources/ContentView.swifttests/test_command_palette_switcher_directory_noise_indexing.pytests_v2/test_command_palette_switcher_home_root_noise.py
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
💡 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".
| commandPaletteHoveredResultIndex = nil | ||
| commandPaletteScrollTargetIndex = nil | ||
| commandPaletteScrollTargetAnchor = nil |
There was a problem hiding this comment.
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 👍 / 👎.
Summary
@Stateinstead of recomputing the full fuzzy scoring pipeline 3-6x per keystroke.id(commandPaletteQuery)that destroyed the entireLazyVStackrow tree on every character typedscripts/populate-debug-workspaces.shfor testing with many workspacesTest plan
./scripts/populate-debug-workspaces.sh /tmp/cmux-debug-cmdp-perf.sockto create 20 workspaces with splits>prefix)Closes #668
Summary by CodeRabbit
Performance Improvements
Bug Fixes
New Features
Tests