Skip to content

Fix missing command palette open-in targets - #1621

Merged
lawrencecchen merged 5 commits into
mainfrom
task-cmd-p-missing-open-actions
Mar 18, 2026
Merged

lawrencecchen merged 5 commits into
mainfrom
task-cmd-p-missing-open-actions

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • fall back to LaunchServices app-name lookup when terminal open targets are installed outside the hardcoded bundle paths
  • compute terminal open-target availability live when building the command palette context instead of freezing it at app launch
  • add regression coverage for VS Code alias and Tower lookup outside standard app locations

Testing

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-task-cmd-p-missing-open-actions-unit-green test -only-testing:cmuxTests/TerminalDirectoryOpenTargetAvailabilityTests
  • ./scripts/reload.sh --tag task-cmd-p-missing-open-actions
  • debug command-palette socket query for find on tagged build, confirmed Open Current Directory in Finder still appears in terminal-scoped results

Summary by cubic

Fixes missing “Open Current Directory in …” actions by resolving apps via LaunchServices and recalculating availability at runtime; all open‑in commands now show in terminal panels even when apps are in non‑standard locations. Keeps the command palette empty state stable while refining a no‑match query to avoid blinking.

  • Bug Fixes
    • Resolve app paths via LaunchServices (NSWorkspace.fullPath(forApplication:)) using names derived from bundle candidates.
    • Compute open‑target availability live (remove cached set).
    • Show all terminal open‑in commands; verify availability at execution time.
    • VS Code: vscode available when the app is found; vscodeInline still requires code-tunnel.
    • Preserve the empty state while results are pending if the new query refines a resolved no‑match; no blinking across palette surfaces.
    • Tests: coverage for VS Code alias and Tower outside /Applications, plus unit and UI tests for the no‑match refinement behavior.

Written for commit b65c794. Summary will update on new commits.

Summary by CodeRabbit

  • Improvements
    • Better application discovery with a system-backed fallback to locate apps outside standard install locations.
    • Command palette now surfaces a broader, dynamic set of terminal targets instead of a static cached list.
  • Bug Fixes
    • Command palette empty-state remains stable (does not blink) while refining no-match queries.
  • Tests
    • Expanded tests covering detection/fallback behavior and VSCode/Tower detection in nonstandard locations, plus command-palette empty-state preservation.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented Mar 17, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 17, 2026 11:30pm

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 17, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a LaunchServices-backed application-name lookup via DetectionEnvironment.applicationPathForName, removes a cached live targets surface, and updates target resolution and UI to use dynamic TerminalDirectoryOpenTarget.availableTargets() with name-based fallback resolution and search-query empty-state preservation.

Changes

Cohort / File(s) Summary
Core Detection Logic
Sources/AppDelegate.swift
Adds applicationPathForName: (String) -> String? to DetectionEnvironment and initializes live with NSWorkspace.shared.fullPath(forApplication:). Removes cachedLiveAvailableTargets. Adds private helpers (applicationSearchNames, applicationPath(in:)) and extends TerminalDirectoryOpenTarget resolution to fallback to name-based lookups.
UI Integration
Sources/ContentView.swift
Replaces uses of TerminalDirectoryOpenTarget.cachedLiveAvailableTargets with TerminalDirectoryOpenTarget.availableTargets(). Adds commandPaletteResolvedMatchingQuery, commandPaletteShouldPreserveEmptyStateWhileSearchPending(...), and commandPaletteShouldShowEmptyState to preserve empty-state during refining searches and wires resolved-matching-query state into search/result resolution.
Unit Tests
cmuxTests/CmuxWebViewKeyEquivalentTests.swift, cmuxTests/CommandPaletteSearchEngineTests.swift
Updates test DetectionEnvironment initializers to accept applicationPathForName. Renames/adjusts VSCode test and adds tests covering application-name fallback detection for VSCode and Tower outside standard app locations. Adds unit tests for commandPaletteShouldPreserveEmptyStateWhileSearchPending.
UI Tests
cmuxUITests/SidebarHelpMenuUITests.swift
Adds testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery, plus helpers debugTypeText and seedWorkspaceSwitcherCorpus to simulate typing and seed workspace data; asserts empty-state visibility during query refinement.
Build Manifest
Package.swift
Minor manifest edits (+/- lines).

Sequence Diagram(s)

sequenceDiagram
    participant CV as ContentView
    participant TOT as TerminalDirectoryOpenTarget
    participant DE as DetectionEnvironment
    participant WS as NSWorkspace

    CV->>TOT: availableTargets()
    TOT->>TOT: resolve candidates for each target
    TOT->>DE: fileExistsAtPath / isExecutableFileAtPath
    alt candidate path resolved
        DE-->>TOT: bundle path
    else fallback to name lookup
        TOT->>TOT: compute applicationSearchNames
        loop per appName
            TOT->>DE: applicationPathForName(appName)
            DE->>WS: fullPath(forApplication:)
            WS-->>DE: app path or nil
            DE-->>TOT: path or nil
        end
    end
    TOT-->>CV: available targets list
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #685: Modifies TerminalDirectoryOpenTarget detection/availability logic and DetectionEnvironment — touches the same resolution paths.
  • #989: Adjusts ContentView command-palette state and rendering logic, overlapping with the new empty-state and resolved-query handling.
  • #1389: Changes VS Code discovery/launch paths and uses NSWorkspace-style lookups, overlapping with the application-name fallback behavior.

Poem

🐰 I hopped through LaunchServices with a twitchy nose,
Sniffed names and found apps where mystery grows.
No cached crumbs left on the floor, just fresh tracks,
Targets find doors beyond the usual stacks. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective of the changeset: fixing missing command palette open-in targets through LaunchServices fallback lookup and live availability computation.
Description check ✅ Passed The description covers the key aspects (what changed, why, testing approach) but lacks detailed manual verification specifics, demo video, and incomplete checklist items.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-cmd-p-missing-open-actions
📝 Coding Plan
  • Generate coding plan for human review comments

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

❤️ Share

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@cmuxUITests/SidebarHelpMenuUITests.swift`:
- Around line 519-535: The test currently uses sidebarHelpPollUntil(timeout:
0.5, ...) to assert the emptyLabel remains visible but then waits up to 5s for
waitForCommandPaletteSnapshot(...) to return refinedSnapshot, allowing the label
to disappear after 0.5s on slow CI; change the logic so the no-blink check runs
until the refined snapshot is observed: remove the fixed 0.5s timeout and
instead poll sidebarHelpPollUntil(...) continuously while
waitForCommandPaletteSnapshot(windowId: mainWindowId, query: String(repeating:
"z", count: 9), ...) is pending (or combine into a single poll condition that
returns true only when the refined snapshot is seen and the emptyLabel still
exists). Update references: emptyLabel, sidebarHelpPollUntil,
waitForCommandPaletteSnapshot, refinedSnapshot, and commandPaletteResultRows so
the empty-state visibility is asserted for the full duration of the refined
snapshot wait.
- Around line 497-498: After calling seedWorkspaceSwitcherCorpus(workspaceCount:
96) ensure the seeded entries are actually indexed by polling the switcher
search until at least one known seeded workspace title appears (or until the
expected count is returned) before proceeding to the no-match assertion;
implement this by performing the same switcher search used later (e.g., call the
switcher search helper or getSwitcherResults/searchInSwitcher) in a retry loop
with a timeout (or use an XCTest expectation/XCTWaiter) and assert the presence
of a seeded workspace title or expected result count; update the same pattern
for the other occurrences (lines ~670-698) so the test fails if indexing didn’t
complete rather than silently using the default workspace set.
- Around line 479-540: The test
testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery asserts a global "no
blink while refining" UX guarantee that touches shared infrastructure
(scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:)); remove or
relax the specific visibility assertion that enforces emptyLabel never
disappearing (the sidebarHelpPollUntil/XCTAssertFalse block checking
emptyLabelDisappearedWhileRefining) so this PR only verifies the
open-target/scoped behavior: keep the setup, ensure the refined query still
yields no results (the waitForCommandPaletteSnapshot/assert that resultRows are
empty) but do not assert on the empty-state label visibility or modify
scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:)'s seeding
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f06457cf-5c40-4694-a1d5-a800e97a5a33

📥 Commits

Reviewing files that changed from the base of the PR and between f370426 and f4cc7c8.

📒 Files selected for processing (1)
  • cmuxUITests/SidebarHelpMenuUITests.swift

Comment on lines +479 to +540
func testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery() throws {
let app = XCUIApplication()
app.launchArguments += ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"]
app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1"
app.launchEnvironment["CMUX_SOCKET_PATH"] = socketPath
launchAndActivate(app)

XCTAssertTrue(
sidebarHelpPollUntil(timeout: 8.0) {
app.windows.count >= 1
},
"Expected the main window to be visible"
)
XCTAssertTrue(waitForSocketPong(timeout: 12.0), "Expected control socket at \(socketPath)")

let mainWindowId = try XCTUnwrap(
socketCommand("current_window")?.trimmingCharacters(in: .whitespacesAndNewlines)
)
try seedWorkspaceSwitcherCorpus(workspaceCount: 96)

let searchField = app.textFields["CommandPaletteSearchField"]
app.typeKey("p", modifierFlags: [.command])
XCTAssertTrue(searchField.waitForExistence(timeout: 5.0), "Expected command palette search field")
searchField.click()

try debugTypeText(String(repeating: "z", count: 8))

let emptyLabel = app.staticTexts["No workspaces match your search."].firstMatch
XCTAssertTrue(
sidebarHelpPollUntil(timeout: 5.0) {
guard emptyLabel.exists else { return false }
guard let snapshot = commandPaletteSnapshot(windowId: mainWindowId) else { return false }
return (snapshot["query"] as? String) == String(repeating: "z", count: 8)
&& self.commandPaletteResultRows(from: snapshot).isEmpty
},
"Expected the switcher to reach a visible no-results state before refining the query"
)

try debugTypeText("z")

let emptyLabelDisappearedWhileRefining = sidebarHelpPollUntil(timeout: 0.5, pollInterval: 0.01) {
!emptyLabel.exists
}
XCTAssertFalse(
emptyLabelDisappearedWhileRefining,
"Expected refining an already-empty switcher query to keep the empty-state label visible"
)

let refinedSnapshot = try XCTUnwrap(
waitForCommandPaletteSnapshot(
windowId: mainWindowId,
query: String(repeating: "z", count: 9),
timeout: 5.0
) { snapshot in
self.commandPaletteResultRows(from: snapshot).isEmpty
}
)
XCTAssertTrue(
commandPaletteResultRows(from: refinedSnapshot).isEmpty,
"Expected the refined no-match query to stay empty. snapshot=\(refinedSnapshot)"
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This regression test is scoped to shared command-palette refresh behavior, not the open-target fix.

It locks a “no blink while refining” guarantee onto shared switcher refresh infrastructure, so this PR now depends on a broader UX behavior that maintainers have explicitly asked to keep out of feature-scoped changes.

Based on learnings, "scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:) is shared command‑palette infrastructure across all submenus. Do not change its sync‑seeding behavior within feature‑scoped PRs; treat brief initial flashes as consistent with existing submenus. Any UX improvement ... should be implemented and tested globally in a dedicated follow‑up PR."

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

In `@cmuxUITests/SidebarHelpMenuUITests.swift` around lines 479 - 540, The test
testSwitcherEmptyStateDoesNotBlinkWhileRefiningNoMatchQuery asserts a global "no
blink while refining" UX guarantee that touches shared infrastructure
(scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:)); remove or
relax the specific visibility assertion that enforces emptyLabel never
disappearing (the sidebarHelpPollUntil/XCTAssertFalse block checking
emptyLabelDisappearedWhileRefining) so this PR only verifies the
open-target/scoped behavior: keep the setup, ensure the refined query still
yields no results (the waitForCommandPaletteSnapshot/assert that resultRows are
empty) but do not assert on the empty-state label visibility or modify
scheduleCommandPaletteResultsRefresh(forceSearchCorpusRefresh:)'s seeding
behavior.

Comment on lines +497 to +498
try seedWorkspaceSwitcherCorpus(workspaceCount: 96)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert that the seeded switcher corpus is actually searchable before the no-match check.

seedWorkspaceSwitcherCorpus only sends new_workspace/workspace.rename; it never waits for those titles to land in switcher results. If indexing lags, this test can pass against the default workspace set and stop exercising the large-corpus path it is supposed to cover.

Also applies to: 670-698

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

In `@cmuxUITests/SidebarHelpMenuUITests.swift` around lines 497 - 498, After
calling seedWorkspaceSwitcherCorpus(workspaceCount: 96) ensure the seeded
entries are actually indexed by polling the switcher search until at least one
known seeded workspace title appears (or until the expected count is returned)
before proceeding to the no-match assertion; implement this by performing the
same switcher search used later (e.g., call the switcher search helper or
getSwitcherResults/searchInSwitcher) in a retry loop with a timeout (or use an
XCTest expectation/XCTWaiter) and assert the presence of a seeded workspace
title or expected result count; update the same pattern for the other
occurrences (lines ~670-698) so the test fails if indexing didn’t complete
rather than silently using the default workspace set.

Comment on lines +519 to +535
let emptyLabelDisappearedWhileRefining = sidebarHelpPollUntil(timeout: 0.5, pollInterval: 0.01) {
!emptyLabel.exists
}
XCTAssertFalse(
emptyLabelDisappearedWhileRefining,
"Expected refining an already-empty switcher query to keep the empty-state label visible"
)

let refinedSnapshot = try XCTUnwrap(
waitForCommandPaletteSnapshot(
windowId: mainWindowId,
query: String(repeating: "z", count: 9),
timeout: 5.0
) { snapshot in
self.commandPaletteResultRows(from: snapshot).isEmpty
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The no-blink assertion stops well before the refined query is allowed to settle.

You only watch the empty-state label for 0.5s, but the refined snapshot is allowed 5s to arrive. On slower CI, the label can disappear after that 0.5s window and this still passes. Keep the visibility check running until the 9-character snapshot is observed.

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

In `@cmuxUITests/SidebarHelpMenuUITests.swift` around lines 519 - 535, The test
currently uses sidebarHelpPollUntil(timeout: 0.5, ...) to assert the emptyLabel
remains visible but then waits up to 5s for waitForCommandPaletteSnapshot(...)
to return refinedSnapshot, allowing the label to disappear after 0.5s on slow
CI; change the logic so the no-blink check runs until the refined snapshot is
observed: remove the fixed 0.5s timeout and instead poll
sidebarHelpPollUntil(...) continuously while
waitForCommandPaletteSnapshot(windowId: mainWindowId, query: String(repeating:
"z", count: 9), ...) is pending (or combine into a single poll condition that
returns true only when the refined snapshot is seen and the emptyLabel still
exists). Update references: emptyLabel, sidebarHelpPollUntil,
waitForCommandPaletteSnapshot, refinedSnapshot, and commandPaletteResultRows so
the empty-state visibility is asserted for the full duration of the refined
snapshot wait.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)

4830-4835: ⚠️ Potential issue | 🟠 Major

Avoid live app discovery in the commands refresh hot path.

Line 4830 now runs TerminalDirectoryOpenTarget.availableTargets() inside commandPaletteContextSnapshot(). That snapshot is read by both commandPaletteCommandsFingerprint() and commandPaletteCommands(), so each commands-mode query update can do the filesystem/LaunchServices lookup more than once before the background search even starts. This is a typing-sensitive path; please compute the set once per refresh or palette session and thread it through instead of doing live discovery from the snapshot.

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

In `@Sources/ContentView.swift` around lines 4830 - 4835, The code calls
TerminalDirectoryOpenTarget.availableTargets() inside
commandPaletteContextSnapshot(), causing repeated live discovery during each
commands refresh; instead compute availableTargets once per refresh/session and
thread that result through the command-palette flow (e.g. call
TerminalDirectoryOpenTarget.availableTargets() once before creating the snapshot
in the refresh path and pass the resulting Set into
commandPaletteContextSnapshot() or include it as an explicit parameter used by
commandPaletteContextSnapshot(), commandPaletteCommandsFingerprint(), and
commandPaletteCommands()); update uses of
CommandPaletteContextKeys.terminalOpenTargetAvailable to read from the
precomputed set rather than invoking availableTargets() again.
🤖 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 4830-4835: The code calls
TerminalDirectoryOpenTarget.availableTargets() inside
commandPaletteContextSnapshot(), causing repeated live discovery during each
commands refresh; instead compute availableTargets once per refresh/session and
thread that result through the command-palette flow (e.g. call
TerminalDirectoryOpenTarget.availableTargets() once before creating the snapshot
in the refresh path and pass the resulting Set into
commandPaletteContextSnapshot() or include it as an explicit parameter used by
commandPaletteContextSnapshot(), commandPaletteCommandsFingerprint(), and
commandPaletteCommands()); update uses of
CommandPaletteContextKeys.terminalOpenTargetAvailable to read from the
precomputed set rather than invoking availableTargets() again.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7adafee-8ec5-4fa4-8146-109328bd2235

📥 Commits

Reviewing files that changed from the base of the PR and between f4cc7c8 and b65c794.

📒 Files selected for processing (2)
  • Sources/ContentView.swift
  • cmuxTests/CommandPaletteSearchEngineTests.swift

@lawrencecchen
lawrencecchen merged commit 4162ecc into main Mar 18, 2026
14 checks passed
@lawrencecchen
lawrencecchen deleted the task-cmd-p-missing-open-actions branch March 18, 2026 00:15

This branch was successfully deployed

1 active deployment
Preview — b65c794c Deployed Mar 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant