Skip to content

Add global hotkey popup terminal - #1353

Open
ogabrielluiz wants to merge 8 commits into
manaflow-ai:mainfrom
ogabrielluiz:feature/popup-terminal
Open

ogabrielluiz wants to merge 8 commits into
manaflow-ai:mainfrom
ogabrielluiz:feature/popup-terminal

Conversation

@ogabrielluiz

@ogabrielluiz ogabrielluiz commented Mar 13, 2026 •

Copy link
Copy Markdown

Summary

Hey all. I wanted to build this feature because it was the only thing keeping me from switching to cmux.

What it does is add a global hotkey popup terminal, similar to what Guake on Linux and Warp on macOS offer. Press F10 from anywhere (even when cmux isn't focused) and a terminal slides in from the top of the screen. Press F10 again and it slides away, restoring focus to whatever app you were using before.

How it works

  • Uses the Carbon RegisterEventHotKey API to capture the hotkey globally. This is the same approach macOS apps like Warp use, and it doesn't require Accessibility or Input Monitoring permissions.
  • The popup is an NSPanel with .floating level that appears above all other windows. When shown, cmux switches to .accessory activation policy so it doesn't appear in the Dock or app switcher.
  • Regular cmux windows are hidden when the popup appears and stay hidden when it dismisses. They come back when you click the dock icon. This keeps the popup fully independent from normal windows.
  • If another app was focused before F10, that app regains focus on hide. If cmux was focused, the app fully deactivates.
  • Auto-hides when you switch to another app (configurable).
  • The panel uses .transient and .ignoresCycle collection behavior so it won't reappear on dock click or Cmd+` cycling.

Settings

All configurable in Preferences:

  • Position: top, bottom, left, or right edge
  • Screen: active screen (follows mouse) or primary screen
  • Width/Height: percentage of screen (10%-100%)
  • Auto-hide on focus loss: on/off
  • Keyboard shortcut: F10 by default, remappable to any function key with optional modifiers
  • Animation duration: 80ms default

What's included

  • PopupTerminalController - singleton managing the panel lifecycle, activation policy, and focus restoration. Includes design notes documenting the three AppKit subsystems that interact and why each step is needed.
  • PopupTerminalSettings - UserDefaults-backed settings with testable accessors (accept injected UserDefaults) and pure frame computation functions extracted for unit testing
  • Carbon hotkey registration in AppDelegate
  • Settings UI in Preferences (matches existing patterns with SettingsCardRow, SettingsPickerRow, ShortcutSettingRow)
  • Socket commands: popup-terminal.toggle, popup-terminal.show, popup-terminal.hide
  • CLI commands: popup-terminal-toggle, popup-terminal-show, popup-terminal-hide
  • Unit tests for settings persistence, enum fallbacks, zero-value defaults, and frame computation for all four positions

Testing

  • Tested locally on macOS with ./scripts/reload.sh --tag popup-terminal
  • Verified all manual test scenarios below
  • Unit tests pass (xcodebuild -scheme cmux-unit)

Manual test plan

  • Press F10 with cmux unfocused: popup should appear and receive keyboard focus
  • Press F10 again: popup should slide away, previous app regains focus
  • With cmux window focused, press F10: regular window hides, popup appears
  • Press F10 again: popup hides, cmux fully deactivates (no windows visible)
  • Press F10 a third time: only popup appears (regular window stays hidden)
  • Click dock icon after hiding popup: regular cmux window comes back
  • Click on another app while popup is visible: popup auto-hides
  • Change position in settings: popup slides from new edge
  • Change shortcut in settings: new shortcut works globally
  • Disable popup terminal in settings: hotkey stops working

Demo Video

  • Video URL or attachment:
CleanShot.2026-03-12.at.10.30.13.mp4

Review Trigger (Copy/Paste as PR comment)

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

Checklist

  • I tested the change locally
  • I added or updated tests for behavior changes
  • I updated docs/changelog if needed
  • I requested bot reviews after my latest commit (copy/paste block above or equivalent)
  • All code review bot comments are resolved
  • All human review comments are resolved

Summary by cubic

Adds a global popup terminal toggled by F10 (or any remapped key) that slides in from any screen edge and stays independent of normal windows. Includes Preferences, socket/CLI commands, tests, and fixes for focus and hotkey behavior.

  • New Features

    • Global hotkey via Carbon RegisterEventHotKey (default F10; remappable to any key with optional modifiers; F-keys allowed without modifiers; no Accessibility/Input Monitoring). Logs registration failures.
    • Floating NSPanel above all apps; app switches to .accessory on show and back on hide. Regular windows hide on show and aren’t restored by the hotkey; focus returns to the previous app on hide. Optional auto-hide on app switch.
    • Preferences: position (top/bottom/left/right), screen (active/primary), width/height %, auto-hide, enable/disable. Live repositioning and auto-hide updates; included in Reset All.
    • API/CLI: socket popup-terminal.toggle|show|hide and CLI popup-terminal-toggle|show|hide. Shortcut handling adds keycode matching for function/special keys and supports non‑F keys. Unit tests for settings persistence and frame computation.
  • Bug Fixes

    • Auto-hide on focus loss no longer restores focus to the wrong app (skips restore when auto-hide triggers).
    • Closing the panel (e.g., Cmd+W) now restores activation policy and focus by routing through hide().
    • Global hotkey now re-registers only when the popup shortcut actually changes (and when enable/disable toggles); CLI help entries and capabilities list updated.

Written for commit 03230a2. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Popup terminal: show/hide/toggle panel with animations, focus restoration, and repositioning.
  • Settings

    • Configurable popup: enable/disable, position, target screen, size, animation duration, auto-hide on app switch; integrated into Settings UI and reset flow.
  • Keyboard Shortcuts

    • Global hotkey support (default F10) with function-key handling and lifecycle management.
  • CLI

    • New commands: popup-terminal-toggle, popup-terminal-show, popup-terminal-hide.
  • Tests

    • Comprehensive unit tests for settings persistence and frame computation.

@vercel

vercel Bot commented Mar 13, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

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

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@ogabrielluiz

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@coderabbitai

coderabbitai Bot commented Mar 13, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a macOS popup terminal: CLI commands and server handlers (toggle/show/hide), a PopupTerminalController with show/hide/reposition and settings persisted in UserDefaults, global Carbon hotkey support (function-key handling, default F10), UI settings integration, and unit tests for settings and frame calculations.

Changes

Cohort / File(s) Summary
CLI Command Wiring
CLI/cmux.swift
Added three new CLI subcommands (popup-terminal-toggle, popup-terminal-show, popup-terminal-hide) that send v2 requests and print responses.
Hotkey & Shortcut Integration
Sources/AppDelegate.swift, Sources/KeyboardShortcutSettings.swift
Added Carbon global hotkey registration/handler in AppDelegate; extended StoredShortcut to accept F1–F12 without modifiers and added togglePopupTerminal action with default F10.
Popup Terminal Controller
Sources/PopupTerminalController.swift
New singleton managing NSPanel lifecycle (toggle/show/hide/reposition), focus capture/restore, activation policy changes, auto-hide on focus loss, and animated on/offscreen frame calculations.
Popup Settings & Persistence
Sources/PopupTerminalSettings.swift
New persisted settings wrapper (position, screen selection, width/height%, auto-hide, animation duration) with UserDefaults accessors and pure frame computation helpers (target/offscreen frames, clamping/validation).
Server Command Routing
Sources/TerminalController.swift
Added V1/V2 routing and handlers for popup-terminal.toggle, .show, and .hide that invoke PopupTerminalController and return visibility state.
Settings UI Integration
Sources/cmuxApp.swift
Added AppStorage-backed popup terminal settings UI section, onChange hooks to re-register hotkey, reposition, and refresh auto-hide; reset-to-default restores popup settings.
Project & Build Files
GhosttyTabs.xcodeproj/project.pbxproj
Added file references and build phase entries for PopupTerminalController.swift, PopupTerminalSettings.swift, and test file wiring.
Tests
cmuxTests/PopupTerminalSettingsTests.swift
New unit tests covering defaults, persistence, enum labels, frame computation, clamping, and offscreen behavior across two test classes.

Sequence Diagram

sequenceDiagram
    actor User
    participant AppDelegate
    participant TerminalController
    participant PopupTerminalController
    participant NSPanel

    User->>AppDelegate: Presses registered hotkey (e.g., F10)
    AppDelegate->>TerminalController: Dispatch v2 "popup-terminal.toggle"
    TerminalController->>PopupTerminalController: toggle()
    alt popup hidden
        PopupTerminalController->>PopupTerminalController: capture previous app & hide regular windows
        PopupTerminalController->>NSPanel: create/retrieve panel and set content
        PopupTerminalController->>NSPanel: animate to target frame & activate
        NSPanel-->>User: popup shown
    else popup visible
        PopupTerminalController->>NSPanel: animate offscreen and hide
        PopupTerminalController->>PopupTerminalController: restore previous app focus
        NSPanel-->>User: popup hidden
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
With a hop I bind F10 to a cheer,
A panel arrives, soft and near,
Settings snug, frames calculated right,
Tests hop in to keep things tight,
Rabbit applauds the popup’s light.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.65% 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 'Add global hotkey popup terminal' directly and clearly summarizes the main feature introduced: a global hotkey-triggered popup terminal.
Description check ✅ Passed The PR description includes all required template sections with comprehensive detail: Summary (what/why), Testing (local testing and manual test scenarios), Demo Video (GitHub-hosted), Review Trigger, and Checklist (mostly completed).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 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 commented Mar 13, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@ogabrielluiz I have started the AI code review. It will take a few minutes to complete.

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

@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 13, 2026

Copy link
Copy Markdown

@ogabrielluiz: I'll review the changes in this PR now!


🧠 Learnings used
Learnt from: SuperManfred
Repo: manaflow-ai/cmux PR: 1150
File: Sources/AppDelegate.swift:7394-7401
Timestamp: 2026-03-10T10:24:14.017Z
Learning: Repo: manaflow-ai/cmux — For Cmd+W behavior with browser popups: BrowserPopupPanel.performKeyEquivalent is the primary handler to close popups. AppDelegate.handleCustomShortcut(_:), in its Cmd+W fallback, must check both NSApp.keyWindow and event.window for identifier "cmux.browser-popup" and close it if found, before routing to workspace/settings close logic.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Add a Guake/Warp-style popup terminal that slides in from a screen edge
via a global hotkey. Uses Carbon RegisterEventHotKey for system-wide F10
capture without requiring Accessibility permissions.

- PopupTerminalController: singleton managing floating NSPanel lifecycle,
  activation policy dance, and focus restoration
- PopupTerminalSettings: UserDefaults-backed settings with testable
  accessors and pure frame computation functions
- Carbon hotkey registration in AppDelegate
- KeyboardShortcutSettings: togglePopupTerminal action with F10 default
Add configurable settings in Preferences for position, screen, size,
auto-hide on focus loss, and animation duration. Includes onChange
handler to re-register hotkey when enable/disable is toggled.
Add popup-terminal.toggle/show/hide v2 socket commands and
corresponding CLI commands for scripting and automation.
Unit tests for UserDefaults persistence (defaults, round-trips, enum
fallbacks) and pure frame computation (all positions, clamping,
offscreen displacement, offset screens).
@ogabrielluiz
ogabrielluiz force-pushed the feature/popup-terminal branch from 6d53667 to b38256c Compare March 13, 2026 11:51

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d53667e55

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

// panel from reappearing if the user clicks the dock icon.
panel.orderOut(nil)

restoreFocus(to: appToRestore)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve user-selected app when auto-hide triggers

When auto-hide fires on NSApplication.didResignActiveNotification, this hide() path still restores previousApp captured at show time. If the popup was opened from app A and the user clicks app B, cmux immediately re-activates app A, stealing focus from the app the user just selected. This makes app switching unreliable whenever autoHideOnFocusLoss is enabled.

Useful? React with 👍 / 👎.

Comment thread Sources/PopupTerminalController.swift Outdated
Comment on lines +292 to +294
self.removeFocusLossObserver()
self.panel = nil
self.isVisible = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Route panel close through full popup hide cleanup

The panel is created as .closable, but windowWillClose only clears controller state; it does not restore activation policy or previously focused app. Closing via the close button/Cmd+W while visible can leave the app in .accessory mode with regular windows still ordered out, which is inconsistent with the normal hide() teardown and can strand users without expected Dock/window restoration behavior.

Useful? React with 👍 / 👎.

Comment thread Sources/cmuxApp.swift
}
}
}
.onChange(of: popupEnabled) { _ in AppDelegate.shared?.registerPopupTerminalHotKey() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-register global hotkey when shortcut changes

This settings section only re-registers the Carbon hotkey when popupEnabled changes. Updating ShortcutSettingRow(action: .togglePopupTerminal) writes a new shortcut to defaults, but no re-registration is triggered here, so the old keybinding remains active until restart (or a manual enable toggle).

Useful? React with 👍 / 👎.

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

🧹 Nitpick comments (3)
Sources/TerminalController.swift (1)

2879-2887: Return observed visibility for show/hide responses (match toggle).

toggle reports actual visibility, but show/hide return constants. Prefer reading PopupTerminalController.shared.isVisible after the action for consistent API behavior.

Proposed refactor
     private func v2PopupTerminalShow(params _: [String: Any]) -> V2CallResult {
         v2MainSync { PopupTerminalController.shared.show() }
-        return .ok(["visible": true])
+        return .ok(["visible": v2MainSync { PopupTerminalController.shared.isVisible }])
     }

     private func v2PopupTerminalHide(params _: [String: Any]) -> V2CallResult {
         v2MainSync { PopupTerminalController.shared.hide() }
-        return .ok(["visible": false])
+        return .ok(["visible": v2MainSync { PopupTerminalController.shared.isVisible }])
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/TerminalController.swift` around lines 2879 - 2887, The show/hide
handlers currently return constant visibility rather than the actual state;
update v2PopupTerminalShow and v2PopupTerminalHide to call
PopupTerminalController.shared.show()/hide() inside v2MainSync and then read
PopupTerminalController.shared.isVisible and return that value (e.g.,
.ok(["visible": PopupTerminalController.shared.isVisible])) so their responses
match the behavior of toggle; ensure you read isVisible after performing the
action to capture the real resulting state.
Sources/AppDelegate.swift (1)

9371-9374: Consider centralizing function-key keyCode mapping to one source of truth.

This map duplicates the F1–F12 mapping logic already maintained in Sources/KeyboardShortcutSettings.swift and can drift over time.

💡 Refactor direction
-    private static let functionKeyCodeMap: [String: UInt16] = [
-        "f1": 122, "f2": 120, "f3": 99, "f4": 118, "f5": 96, "f6": 97,
-        "f7": 98, "f8": 100, "f9": 101, "f10": 109, "f11": 103, "f12": 111,
-    ]
+    private static let functionKeyCodeMap: [String: UInt16] = KeyboardShortcutSettings.functionKeyCodeMap

(Or expose a shared helper from one module and consume it in both places.)

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

In `@Sources/AppDelegate.swift` around lines 9371 - 9374, The function-key code
map defined as private static let functionKeyCodeMap in AppDelegate duplicates
the mapping in KeyboardShortcutSettings; remove the duplicate and reference a
single source of truth instead—expose the existing mapping from
KeyboardShortcutSettings (for example by adding a public static property or
helper like KeyboardShortcutSettings.functionKeyCodeMap or a dedicated
KeyboardKeyCodes helper) and update AppDelegate to use that exposed property;
ensure the visibility is public/internal as needed and update any callers to
import or reference the shared symbol so only one mapping is maintained.
cmuxTests/PopupTerminalSettingsTests.swift (1)

209-232: Consider adding negative-percentage clamping cases.

You already test low positive and >100 values; adding negative inputs would harden edge-case coverage for persisted corrupt values.

➕ Optional test addition
+    func testNegativePercentagesClampToMinimum10Percent() {
+        let frame = PopupTerminalSettings.computeTargetFrame(
+            position: .top, widthPercent: -25, heightPercent: -10,
+            visibleFrame: standardScreen
+        )
+        XCTAssertEqual(frame.width, 1920 * 0.1, accuracy: 0.01)
+        XCTAssertEqual(frame.height, 1080 * 0.1, accuracy: 0.01)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/PopupTerminalSettingsTests.swift` around lines 209 - 232, Add tests
that pass negative widthPercent and heightPercent into
PopupTerminalSettings.computeTargetFrame to ensure they are clamped to the
minimum (10%). Specifically, add one test calling computeTargetFrame with
widthPercent: -50, heightPercent: 50 and assert frame.width == 1920 * 0.1
(accuracy 0.01), and another with widthPercent: 50, heightPercent: -20 and
assert frame.height == 1080 * 0.1 (accuracy 0.01); reference the existing test
naming pattern (e.g., testWidthPercentClampedToMinimum10Percent /
testHeightPercentClampedToMinimum10Percent) and reuse standardScreen as the
visibleFrame.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLI/cmux.swift`:
- Around line 1770-1782: The subcommand help system doesn't recognize the new
popup-terminal commands because subcommandUsage(_:) lacks cases for
"popup-terminal-toggle", "popup-terminal-show", and "popup-terminal-hide";
update subcommandUsage(_:) to include these three cases returning appropriate
usage/help text (matching the style of the other entries) so that
dispatchSubcommandHelp(...) can display help for cmux popup-terminal-*. Use the
existing patterns in subcommandUsage(_:) for naming and formatting to ensure
consistent help output.

In `@GhosttyTabs.xcodeproj/project.pbxproj`:
- Line 101: The PBX object IDs for PopupTerminalSettingsTests.swift are
duplicated with GhosttyEnsureFocusWindowActivationTests.swift (PBXBuildFile
entries and fileRef IDs); to fix, assign new unique PBX IDs for each duplicate
occurrence related to PopupTerminalSettingsTests.swift (both the PBXBuildFile
and its fileRef) and update every matching reference in the project.pbxproj so
they no longer reuse the IDs from GhosttyEnsureFocusWindowActivationTests.swift,
ensuring PBXBuildFile/fileRef pairs (and any other duplicated IDs at the
mentioned occurrences) are unique across the file.

In `@Sources/AppDelegate.swift`:
- Around line 7546-7548: Replace the debug print call inside the guard that
checks Self.functionKeyCodeMap (the branch that prints "[PopupTerminal] Shortcut
key '\(shortcut.key)' is not a function key; hotkey not registered") with a
dlog() call and wrap that call in `#if` DEBUG / `#endif`; do the same for the other
similar print usage later in the same scope (the second diagnostic that reports
an unregistered hotkey), ensuring you call dlog(...) with the same formatted
message instead of print(...).
- Around line 7579-7620: The installCarbonHotKeyHandler currently sets
Self.carbonHandlerInstalled = true before calling InstallEventHandler and does
not capture the returned handler, so failures permanently block retries and
prevent cleanup; change the flow in installCarbonHotKeyHandler to call
InstallEventHandler first, check its OSStatus result, and only set
Self.carbonHandlerInstalled = true when status == noErr; also capture the
created EventHandlerRef (or store it in a new static var like
Self.carbonEventHandlerRef) so you can remove the handler later (using
RemoveEventHandler) on teardown or on failure, and if InstallEventHandler fails,
log the error and leave carbonHandlerInstalled false to allow retries.

In `@Sources/cmuxApp.swift`:
- Around line 3100-3105: resetAllSettings() currently doesn't clear the new
popup-terminal AppStorage properties, so add logic to reset popupEnabled,
popupPosition, popupScreen, popupWidth, popupHeight, and popupAutoHide back to
their defaults (use PopupTerminalSettings.defaultEnabled,
.defaultPosition.rawValue, .defaultScreen.rawValue, .defaultWidthPercent,
.defaultHeightPercent, and .defaultAutoHideOnFocusLoss respectively) inside
resetAllSettings() so the popup-terminal state is cleared on a true "Reset All".
- Around line 3501-3506: Add immediate propagation for popupAutoHide changes:
implement a PopupTerminalController method refreshAutoHideBehavior() that checks
isVisible and then calls installFocusLossObserver() when
PopupTerminalSettings.autoHideOnFocusLoss is true or removeFocusLossObserver()
when false, and wire the UI by adding an .onChange(of: popupAutoHide) { _ in
PopupTerminalController.shared.refreshAutoHideBehavior() } alongside the
existing .onChange handlers so changing popupAutoHide updates focus-loss
behavior instantly.

In `@Sources/PopupTerminalController.swift`:
- Around line 72-75: getOrCreatePanel currently can return a brand-new NSPanel
via `panel ?? NSPanel()` without assigning it to the controller, which lets
`show()` proceed but leaves `self.panel` nil so `hide()` no-ops; update
getOrCreatePanel (the method referenced) to always assign any newly created
panel to `self.panel` before returning (or return an optional and let `show()`
guard-fail), ensuring `self.panel` is the same instance used by `show()` and
`hide()` (referencing the `panel` property, `show()`, and `hide()` to locate the
logic).

In `@Sources/PopupTerminalSettings.swift`:
- Around line 110-118: The widthPercent(defaults:) and heightPercent(defaults:)
accessors currently only check for >0, but must clamp persisted values to the
10–100 range to match runtime layout; update both functions (referencing
widthPercentKey, heightPercentKey, defaultWidthPercent, defaultHeightPercent) to
read the stored Double, and if it's <=0 return the respective default, otherwise
clamp the value into the inclusive range 10...100 (e.g. using min/max or a clamp
helper) and return that clamped value so stored and effective values remain
consistent.

In `@Sources/TerminalController.swift`:
- Around line 1995-2002: v2Capabilities() does not advertise the newly added RPC
handlers (v2PopupTerminalToggle, v2PopupTerminalShow, v2PopupTerminalHide), so
capability-driven clients will ignore them; update the v2Capabilities()
implementation to include the strings "popup-terminal.toggle",
"popup-terminal.show", and "popup-terminal.hide" in the returned capabilities
list/set so these methods are reported as supported alongside the other
capabilities.

---

Nitpick comments:
In `@cmuxTests/PopupTerminalSettingsTests.swift`:
- Around line 209-232: Add tests that pass negative widthPercent and
heightPercent into PopupTerminalSettings.computeTargetFrame to ensure they are
clamped to the minimum (10%). Specifically, add one test calling
computeTargetFrame with widthPercent: -50, heightPercent: 50 and assert
frame.width == 1920 * 0.1 (accuracy 0.01), and another with widthPercent: 50,
heightPercent: -20 and assert frame.height == 1080 * 0.1 (accuracy 0.01);
reference the existing test naming pattern (e.g.,
testWidthPercentClampedToMinimum10Percent /
testHeightPercentClampedToMinimum10Percent) and reuse standardScreen as the
visibleFrame.

In `@Sources/AppDelegate.swift`:
- Around line 9371-9374: The function-key code map defined as private static let
functionKeyCodeMap in AppDelegate duplicates the mapping in
KeyboardShortcutSettings; remove the duplicate and reference a single source of
truth instead—expose the existing mapping from KeyboardShortcutSettings (for
example by adding a public static property or helper like
KeyboardShortcutSettings.functionKeyCodeMap or a dedicated KeyboardKeyCodes
helper) and update AppDelegate to use that exposed property; ensure the
visibility is public/internal as needed and update any callers to import or
reference the shared symbol so only one mapping is maintained.

In `@Sources/TerminalController.swift`:
- Around line 2879-2887: The show/hide handlers currently return constant
visibility rather than the actual state; update v2PopupTerminalShow and
v2PopupTerminalHide to call PopupTerminalController.shared.show()/hide() inside
v2MainSync and then read PopupTerminalController.shared.isVisible and return
that value (e.g., .ok(["visible": PopupTerminalController.shared.isVisible])) so
their responses match the behavior of toggle; ensure you read isVisible after
performing the action to capture the real resulting state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6bb3fe34-d267-4e29-ba46-ee4601cf45c5

📥 Commits

Reviewing files that changed from the base of the PR and between e94daa0 and b38256c.

📒 Files selected for processing (9)
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/PopupTerminalController.swift
  • Sources/PopupTerminalSettings.swift
  • Sources/TerminalController.swift
  • Sources/cmuxApp.swift
  • cmuxTests/PopupTerminalSettingsTests.swift

Comment thread CLI/cmux.swift
Comment thread GhosttyTabs.xcodeproj/project.pbxproj Outdated
Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/AppDelegate.swift
Comment on lines +7579 to +7620
private func installCarbonHotKeyHandler() {
guard !Self.carbonHandlerInstalled else { return }
Self.carbonHandlerInstalled = true

var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
InstallEventHandler(
GetApplicationEventTarget(),
{ _, event, _ -> OSStatus in
var hotKeyID = EventHotKeyID()
let status = GetEventParameter(
event,
EventParamName(kEventParamDirectObject),
EventParamType(typeEventHotKeyID),
nil,
MemoryLayout<EventHotKeyID>.size,
nil,
&hotKeyID
)
guard status == noErr else { return status }

// Check if this is our popup terminal hotkey.
// Carbon RegisterEventHotKey consumes the key event at the system
// level, so no window (local or popup) ever sees the registered
// hotkey as a keyDown. This handler is the sole toggle path for
// the popup terminal.
if hotKeyID.signature == AppDelegate.popupTerminalHotKeyID.signature,
hotKeyID.id == AppDelegate.popupTerminalHotKeyID.id {
// Carbon event handlers on GetApplicationEventTarget() run
// on the main thread, so we can call toggle directly.
if PopupTerminalSettings.isEnabled {
PopupTerminalController.shared.toggle()
}
return noErr
}

return OSStatus(eventNotHandledErr)
},
1,
&eventType,
nil,
nil
)

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current Carbon handler installation flow and status handling.
rg -n -C4 'carbonHandlerInstalled|InstallEventHandler\(|registerPopupTerminalHotKey\(' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 2052


🏁 Script executed:

#!/bin/bash
# Check the unregisterPopupTerminalHotKey implementation and globalHotkeyRef usage
rg -n -B2 -A10 'globalHotkeyRef|UnregisterEventHotKey' Sources/AppDelegate.swift | head -50

Repository: manaflow-ai/cmux

Length of output: 1881


🏁 Script executed:

#!/bin/bash
# Get the complete installCarbonHotKeyHandler function
rg -n -A 45 'private func installCarbonHotKeyHandler' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 2249


Check InstallEventHandler status before setting carbonHandlerInstalled.

carbonHandlerInstalled is set to true before verifying handler installation succeeds. If InstallEventHandler fails, retries are permanently blocked and the hotkey system is effectively dead. The handler reference is also not captured, making cleanup impossible if needed.

Suggested fix
 private func installCarbonHotKeyHandler() {
     guard !Self.carbonHandlerInstalled else { return }
-    Self.carbonHandlerInstalled = true

     var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
-    InstallEventHandler(
+    var handlerRef: EventHandlerRef?
+    let status = InstallEventHandler(
         GetApplicationEventTarget(),
         { _, event, _ -> OSStatus in
             var hotKeyID = EventHotKeyID()
             let status = GetEventParameter(
                 event,
                 EventParamName(kEventParamDirectObject),
                 EventParamType(typeEventHotKeyID),
                 nil,
                 MemoryLayout<EventHotKeyID>.size,
                 nil,
                 &hotKeyID
             )
             guard status == noErr else { return status }
 
             // Check if this is our popup terminal hotkey.
             // Carbon RegisterEventHotKey consumes the key event at the system
             // level, so no window (local or popup) ever sees the registered
             // hotkey as a keyDown. This handler is the sole toggle path for
             // the popup terminal.
             if hotKeyID.signature == AppDelegate.popupTerminalHotKeyID.signature,
                hotKeyID.id == AppDelegate.popupTerminalHotKeyID.id {
                 // Carbon event handlers on GetApplicationEventTarget() run
                 // on the main thread, so we can call toggle directly.
                 if PopupTerminalSettings.isEnabled {
                     PopupTerminalController.shared.toggle()
                 }
                 return noErr
             }
 
             return OSStatus(eventNotHandledErr)
         },
         1,
         &eventType,
         nil,
-        nil
+        &handlerRef
     )
+    guard status == noErr else { return }
+    Self.carbonHandlerInstalled = true
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 7579 - 7620, The
installCarbonHotKeyHandler currently sets Self.carbonHandlerInstalled = true
before calling InstallEventHandler and does not capture the returned handler, so
failures permanently block retries and prevent cleanup; change the flow in
installCarbonHotKeyHandler to call InstallEventHandler first, check its OSStatus
result, and only set Self.carbonHandlerInstalled = true when status == noErr;
also capture the created EventHandlerRef (or store it in a new static var like
Self.carbonEventHandlerRef) so you can remove the handler later (using
RemoveEventHandler) on teardown or on failure, and if InstallEventHandler fails,
log the error and leave carbonHandlerInstalled false to allow retries.

Comment thread Sources/cmuxApp.swift
Comment thread Sources/cmuxApp.swift
Comment on lines +3501 to +3506
.onChange(of: popupEnabled) { _ in AppDelegate.shared?.registerPopupTerminalHotKey() }
.onChange(of: popupPosition) { _ in PopupTerminalController.shared.reposition() }
.onChange(of: popupScreen) { _ in PopupTerminalController.shared.reposition() }
.onChange(of: popupWidth) { _ in PopupTerminalController.shared.reposition() }
.onChange(of: popupHeight) { _ in PopupTerminalController.shared.reposition() }
}

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

Apply auto-hide preference changes immediately while the popup is visible.

Line 3501–Line 3505 reacts to most popup settings, but popupAutoHide changes are not propagated. If the panel is already shown, focus-loss behavior stays stale until the next toggle cycle.

Proposed fix
         .onChange(of: popupEnabled) { _ in AppDelegate.shared?.registerPopupTerminalHotKey() }
         .onChange(of: popupPosition) { _ in PopupTerminalController.shared.reposition() }
         .onChange(of: popupScreen) { _ in PopupTerminalController.shared.reposition() }
         .onChange(of: popupWidth) { _ in PopupTerminalController.shared.reposition() }
         .onChange(of: popupHeight) { _ in PopupTerminalController.shared.reposition() }
+        .onChange(of: popupAutoHide) { _ in PopupTerminalController.shared.refreshAutoHideBehavior() }
// Add in PopupTerminalController
func refreshAutoHideBehavior() {
    guard isVisible else { return }
    if PopupTerminalSettings.autoHideOnFocusLoss {
        installFocusLossObserver()
    } else {
        removeFocusLossObserver()
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/cmuxApp.swift` around lines 3501 - 3506, Add immediate propagation
for popupAutoHide changes: implement a PopupTerminalController method
refreshAutoHideBehavior() that checks isVisible and then calls
installFocusLossObserver() when PopupTerminalSettings.autoHideOnFocusLoss is
true or removeFocusLossObserver() when false, and wire the UI by adding an
.onChange(of: popupAutoHide) { _ in
PopupTerminalController.shared.refreshAutoHideBehavior() } alongside the
existing .onChange handlers so changing popupAutoHide updates focus-loss
behavior instantly.

Comment on lines +72 to +75
func show() {
isAnimating = true
let panel = getOrCreatePanel()

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

Don’t return an untracked fallback panel when AppDelegate.shared is nil.

At Line 184, panel ?? NSPanel() can return a fresh panel that is never stored in self.panel. If that path is hit, show() can mark visible, but hide() later no-ops because self.panel is nil.

Proposed fix
-func show() {
+func show() {
     isAnimating = true
-    let panel = getOrCreatePanel()
+    guard let panel = getOrCreatePanel() else {
+        isAnimating = false
+        return
+    }
-private func getOrCreatePanel() -> NSPanel {
+private func getOrCreatePanel() -> NSPanel? {
     if let existing = panel {
         return existing
     }
@@
-    guard let appDelegate = AppDelegate.shared else {
-        return panel ?? NSPanel()
-    }
+    guard let appDelegate = AppDelegate.shared else {
+        return nil
+    }
@@
-    return newPanel
+    return newPanel
 }

Also applies to: 172-185

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

In `@Sources/PopupTerminalController.swift` around lines 72 - 75, getOrCreatePanel
currently can return a brand-new NSPanel via `panel ?? NSPanel()` without
assigning it to the controller, which lets `show()` proceed but leaves
`self.panel` nil so `hide()` no-ops; update getOrCreatePanel (the method
referenced) to always assign any newly created panel to `self.panel` before
returning (or return an optional and let `show()` guard-fail), ensuring
`self.panel` is the same instance used by `show()` and `hide()` (referencing the
`panel` property, `show()`, and `hide()` to locate the logic).

Comment on lines +110 to +118
static func widthPercent(defaults: UserDefaults) -> Double {
let val = defaults.double(forKey: widthPercentKey)
return val > 0 ? val : defaultWidthPercent
}

static func heightPercent(defaults: UserDefaults) -> Double {
let val = defaults.double(forKey: heightPercentKey)
return val > 0 ? val : defaultHeightPercent
}

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

Clamp persisted percent settings to 10–100 to avoid UI/runtime drift.

At Line 110 and Line 115, values are only validated as > 0. Values like 5 or 500 remain persisted and shown in UI, while runtime layout later clamps to 10–100. Please clamp at accessor level too so stored and effective values stay consistent.

Proposed fix
 static func widthPercent(defaults: UserDefaults) -> Double {
     let val = defaults.double(forKey: widthPercentKey)
-    return val > 0 ? val : defaultWidthPercent
+    let resolved = val > 0 ? val : defaultWidthPercent
+    return min(100, max(10, resolved))
 }

 static func heightPercent(defaults: UserDefaults) -> Double {
     let val = defaults.double(forKey: heightPercentKey)
-    return val > 0 ? val : defaultHeightPercent
+    let resolved = val > 0 ? val : defaultHeightPercent
+    return min(100, max(10, resolved))
 }
📝 Committable suggestion

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

Suggested change
static func widthPercent(defaults: UserDefaults) -> Double {
let val = defaults.double(forKey: widthPercentKey)
return val > 0 ? val : defaultWidthPercent
}
static func heightPercent(defaults: UserDefaults) -> Double {
let val = defaults.double(forKey: heightPercentKey)
return val > 0 ? val : defaultHeightPercent
}
static func widthPercent(defaults: UserDefaults) -> Double {
let val = defaults.double(forKey: widthPercentKey)
let resolved = val > 0 ? val : defaultWidthPercent
return min(100, max(10, resolved))
}
static func heightPercent(defaults: UserDefaults) -> Double {
let val = defaults.double(forKey: heightPercentKey)
let resolved = val > 0 ? val : defaultHeightPercent
return min(100, max(10, resolved))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/PopupTerminalSettings.swift` around lines 110 - 118, The
widthPercent(defaults:) and heightPercent(defaults:) accessors currently only
check for >0, but must clamp persisted values to the 10–100 range to match
runtime layout; update both functions (referencing widthPercentKey,
heightPercentKey, defaultWidthPercent, defaultHeightPercent) to read the stored
Double, and if it's <=0 return the respective default, otherwise clamp the value
into the inclusive range 10...100 (e.g. using min/max or a clamp helper) and
return that clamped value so stored and effective values remain consistent.

Comment thread Sources/TerminalController.swift

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

5 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/cmuxApp.swift">

<violation number="1" location="Sources/cmuxApp.swift:3501">
P2: Changing the popup-terminal shortcut does not re-register the global hotkey, so the old key binding remains active.</violation>
</file>

<file name="Sources/PopupTerminalController.swift">

<violation number="1" location="Sources/PopupTerminalController.swift:115">
P1: Auto-hide on focus loss restores focus to the old app, which can steal focus from the app the user just clicked.</violation>

<violation number="2" location="Sources/PopupTerminalController.swift:290">
P2: Closing the popup window directly bypasses `hide()`, so activation policy may stay `.accessory` and app visibility state becomes inconsistent.</violation>
</file>

<file name="GhosttyTabs.xcodeproj/project.pbxproj">

<violation number="1" location="GhosttyTabs.xcodeproj/project.pbxproj:101">
P1: The new test target entries reuse existing PBX UUIDs, causing project object ID collisions.</violation>
</file>

<file name="Sources/TerminalController.swift">

<violation number="1" location="Sources/TerminalController.swift:2885">
P2: `popup-terminal.hide` calls `hide()` directly and bypasses the `toggle()` animation guard. A hide request during show animation can run through an unsupported transition path and destabilize popup focus-loss observer lifecycle.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/PopupTerminalController.swift
Comment thread GhosttyTabs.xcodeproj/project.pbxproj Outdated
Comment thread Sources/cmuxApp.swift
Comment thread Sources/PopupTerminalController.swift
}

private func v2PopupTerminalHide(params _: [String: Any]) -> V2CallResult {
v2MainSync { PopupTerminalController.shared.hide() }

@cubic-dev-ai cubic-dev-ai Bot Mar 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: popup-terminal.hide calls hide() directly and bypasses the toggle() animation guard. A hide request during show animation can run through an unsupported transition path and destabilize popup focus-loss observer lifecycle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalController.swift, line 2885:

<comment>`popup-terminal.hide` calls `hide()` directly and bypasses the `toggle()` animation guard. A hide request during show animation can run through an unsupported transition path and destabilize popup focus-loss observer lifecycle.</comment>

<file context>
@@ -2861,6 +2869,23 @@ class TerminalController {
+    }
+
+    private func v2PopupTerminalHide(params _: [String: Any]) -> V2CallResult {
+        v2MainSync { PopupTerminalController.shared.hide() }
+        return .ok(["visible": false])
+    }
</file context>
Fix with Cubic

- Fix auto-hide restoring focus to wrong app (restore skipped when
  auto-hide triggers, since user already switched to another app)
- Route windowWillClose through hide() so activation policy and
  focus are properly restored on panel close via Cmd+W
- Re-register global hotkey when shortcut changes in settings
- Fix PBX UUID collision with GhosttyEnsureFocusWindowActivationTests
- Add popup-terminal commands to system.capabilities
- Add CLI help entries for popup-terminal-toggle/show/hide
- Include popup settings in resetAllSettings()
- Add refreshAutoHideBehavior() for live auto-hide toggle
- Use dlog() instead of print() per project conventions
@ogabrielluiz

Copy link
Copy Markdown
Author

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Mar 13, 2026

Copy link
Copy Markdown

@codex review
@coderabbitai review
@greptile-apps review
@cubic-dev-ai review

@ogabrielluiz I have started the AI code review. It will take a few minutes to complete.

@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 13, 2026

Copy link
Copy Markdown

@ogabrielluiz: I'll review the changes in this PR now!


🧠 Learnings used
Learnt from: SuperManfred
Repo: manaflow-ai/cmux PR: 1150
File: Sources/AppDelegate.swift:7394-7401
Timestamp: 2026-03-10T10:24:14.017Z
Learning: Repo: manaflow-ai/cmux — For Cmd+W behavior with browser popups: BrowserPopupPanel.performKeyEquivalent is the primary handler to close popups. AppDelegate.handleCustomShortcut(_:), in its Cmd+W fallback, must check both NSApp.keyWindow and event.window for identifier "cmux.browser-popup" and close it if found, before routing to workspace/settings close logic.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/ContentView.swift:3194-3196
Timestamp: 2026-03-04T14:05:48.668Z
Learning: In manaflow-ai/cmux (PR `#819`), Sources/ContentView.swift: The command palette’s external window labels intentionally use the global window index from the full orderedSummaries (index + 1), matching the Window menu in AppDelegate. Do not reindex after filtering out the current window to avoid mismatches (“Window 2” for an external window is expected).

Learnt from: moyashin63
Repo: manaflow-ai/cmux PR: 1074
File: Sources/AppDelegate.swift:7523-7545
Timestamp: 2026-03-09T01:38:35.335Z
Learning: In manaflow-ai/cmux (Sources/AppDelegate.swift), shouldConsumeShortcutWhileCommandPaletteVisible(...) intentionally consumes most Command shortcuts while the command palette is visible to protect its text input. As a result, UI zoom shortcuts (Cmd+Shift+= / Cmd+Shift+− / Cmd+Shift+0) must not fire while the palette is open. Do not reorder handlers (e.g., uiZoomShortcutAction(...)) to bypass this guard; users close the palette before zooming.

Learnt from: debgotwired
Repo: manaflow-ai/cmux PR: 1149
File: Sources/ContentView.swift:3977-3978
Timestamp: 2026-03-10T09:33:37.952Z
Learning: In manaflow-ai/cmux (Sources/ContentView.swift), 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 (e.g., synchronous seeding on forced corpus refresh) should be implemented and tested globally in a dedicated follow‑up PR.

Learnt from: 0xble
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-09T21:53:05.451Z
Learning: Repo: manaflow-ai/cmux — For browser portals, visibility resync is centralized in BrowserWindowPortalRegistry.updateEntryVisibleInUI(for: WKWebView, visibleInUI: Bool), which immediately synchronizes the WKWebView on visibility changes. Call sites (e.g., Workspace.reconcilePanelPortalVisibilityForCurrentLayout()) should use this helper instead of ad‑hoc resync logic.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: CLI/cmux.swift:462-499
Timestamp: 2026-03-09T02:08:54.956Z
Learning: Repo: manaflow-ai/cmux
PR: `#1089`
File: CLI/cmux.swift
Component: ClaudeHookTagExtractor.extractTags(subtitle:body:)
Learning: For Claude Code session tag extraction, pre-redact sensitive spans (UUIDs, emails, access tokens, filesystem paths, ENV_VAR=..., long numerics) across the combined body+subtitle using unanchored sensitiveSpanPatterns before tokenization. Then tokenize and still filter each token with anchored sensitivePatterns. Rationale: prevents PII/path fragments from slipping into searchable tags after delimiter splitting.

Learnt from: atani
Repo: manaflow-ai/cmux PR: 819
File: Sources/AppDelegate.swift:3491-3493
Timestamp: 2026-03-04T14:06:16.241Z
Learning: For manaflow-ai/cmux PR `#819` (Japanese i18n), keep scope limited to localization changes; UX enhancements like preferring workspace.customTitle in workspaceDisplayName() or altering move-target labels should be handled in a separate follow-up issue.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: Sources/TerminalController.swift:3180-3193
Timestamp: 2026-03-09T02:08:14.574Z
Learning: Repo: manaflow-ai/cmux — In Sources/TerminalController.swift, v2WorkspaceClearTags(params:) must only clear all tags when the "source" key is absent. If "source" is present but blank or non-string (v2String(...) returns nil), the API should return invalid_params. Current implementation uses hasSourceKey = params.keys.contains("source") and guards with if hasSourceKey && source == nil { return .err(...)}.

Learnt from: homanp
Repo: manaflow-ai/cmux PR: 883
File: CLI/cmux.swift:1569-1586
Timestamp: 2026-03-04T22:05:07.913Z
Learning: In manaflow-ai/cmux CLI (CLI/cmux.swift), for parity with existing browser commands, markdown.open intentionally normalizes --surface before resolving --workspace/--window, relying on server-side resolution to disambiguate final routing. Avoid one-off reordering in markdown; consider any change only as a coordinated, cross-command refactor.

Learnt from: apollow
Repo: manaflow-ai/cmux PR: 1089
File: Sources/ContentView.swift:6055-6057
Timestamp: 2026-03-09T02:09:08.742Z
Learning: In manaflow-ai/cmux (Sources/ContentView.swift), within CommandPaletteSwitcherSearchIndexer.metadataKeywordsForSearch(_:detail:), when tags are present, only add "tag" and "topic" to contextKeywords; do not append "claude" unconditionally to avoid false-positive switcher matches.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcfcea9a24

ℹ️ 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 +172 to +175
case .top:
return targetFrame.offsetBy(dx: 0, dy: targetFrame.height)
case .bottom:
return targetFrame.offsetBy(dx: 0, dy: -targetFrame.height)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move top/bottom popup fully off-screen before animation

For .top/.bottom, computeOffscreenFrame shifts by targetFrame.height instead of using the screen bounds, so when visibleFrame is inset by the menu bar or Dock, part of the panel can still be on-screen before the slide animation starts. On typical macOS layouts this produces a visible strip/flicker and a shortened slide distance, unlike the left/right branches that account for screenFrame explicitly.

Useful? React with 👍 / 👎.

Comment thread Sources/AppDelegate.swift Outdated
Comment on lines +7546 to +7550
guard let fkCode = Self.functionKeyCodeMap[key] else {
#if DEBUG
dlog("popupTerminal.hotkey skipped reason=nonFunctionKey key=\(shortcut.key)")
#endif
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate popup shortcut keys before attempting registration

This path silently skips registration for any non-function key, but the popup shortcut is configured through the generic recorder, which still allows combinations like ⌘K; users can save such a shortcut and end up with a non-working global hotkey with no feedback. That creates an easy-to-hit broken configuration state until they manually reset to an F key, so this should be rejected or surfaced at save time.

Useful? React with 👍 / 👎.

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

♻️ Duplicate comments (1)
Sources/AppDelegate.swift (1)

7583-7624: ⚠️ Potential issue | 🟠 Major

carbonHandlerInstalled is set too early in handler installation flow.

Line 7585 marks installation complete before confirming InstallEventHandler succeeded. If install fails, retries are blocked and the hotkey path can remain dead for the rest of the process. (Downstream: hotkey registration can still proceed without a working dispatcher.)

Suggested fix
-    private static var carbonHandlerInstalled = false
-    private func installCarbonHotKeyHandler() {
-        guard !Self.carbonHandlerInstalled else { return }
-        Self.carbonHandlerInstalled = true
+    private static var carbonHandlerInstalled = false
+    private static var carbonEventHandlerRef: EventHandlerRef?
+    private func installCarbonHotKeyHandler() -> Bool {
+        guard !Self.carbonHandlerInstalled else { return true }

         var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
-        InstallEventHandler(
+        var handlerRef: EventHandlerRef?
+        let status = InstallEventHandler(
             GetApplicationEventTarget(),
             { _, event, _ -> OSStatus in
                 var hotKeyID = EventHotKeyID()
@@
             1,
             &eventType,
             nil,
-            nil
+            &handlerRef
         )
+        guard status == noErr else {
+#if DEBUG
+            dlog("popupTerminal.hotkey handlerInstallFailed status=\(status)")
+#endif
+            return false
+        }
+        Self.carbonEventHandlerRef = handlerRef
+        Self.carbonHandlerInstalled = true
+        return true
     }
-        installCarbonHotKeyHandler()
+        guard installCarbonHotKeyHandler() else { return }
#!/bin/bash
# Verify current Carbon handler installation flow and status handling.
rg -n -A55 -B8 'private func installCarbonHotKeyHandler\(' Sources/AppDelegate.swift

# Verify whether installed flag is set before checking install status.
rg -n 'carbonHandlerInstalled = true|InstallEventHandler\(|status == noErr|EventHandlerRef|RemoveEventHandler' Sources/AppDelegate.swift

Expected after fix: carbonHandlerInstalled = true appears only after a status == noErr guard.

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

In `@Sources/AppDelegate.swift` around lines 7583 - 7624,
installCarbonHotKeyHandler currently sets Self.carbonHandlerInstalled = true
before calling InstallEventHandler, which means a failed InstallEventHandler
will permanently mark the handler installed; change the flow so you call
InstallEventHandler first, capture its returned OSStatus (and any
EventHandlerRef if used), check status == noErr, and only then set
Self.carbonHandlerInstalled = true; ensure you return or log the error and leave
the flag false when InstallEventHandler fails (referencing
installCarbonHotKeyHandler, Self.carbonHandlerInstalled, and InstallEventHandler
to locate the code).
🧹 Nitpick comments (1)
Sources/cmuxApp.swift (1)

3507-3512: Scope hotkey re-registration to actual shortcut changes.

Line 3507 currently reacts to every UserDefaults mutation, triggering unnecessary unregister/register cycles in registerPopupTerminalHotKey() during unrelated settings edits. Gate this observer on .togglePopupTerminal shortcut changes only by tracking the last known shortcut value and re-registering only when it actually changes.

This pattern already exists elsewhere in cmuxApp (lines 4933–4937):

.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
    let latest = KeyboardShortcutSettings.shortcut(for: action)
    if latest != shortcut {
        shortcut = latest
    }
}

Apply the same approach: add a @State to cache the last popup terminal shortcut and compare on each notification.

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

In `@Sources/cmuxApp.swift` around lines 3507 - 3512, The current observer
unconditionally calls AppDelegate.shared?.registerPopupTerminalHotKey() on any
UserDefaults change; instead add a `@State` property (e.g., popupShortcut) to
cache KeyboardShortcutSettings.shortcut(for: .togglePopupTerminal) and in the
.onReceive(NotificationCenter.default.publisher(for:
UserDefaults.didChangeNotification)) handler read the latest via
KeyboardShortcutSettings.shortcut(for: .togglePopupTerminal) and only call
registerPopupTerminalHotKey() (and update popupShortcut) when the latest !=
popupShortcut; this mirrors the existing pattern used elsewhere (lines using
KeyboardShortcutSettings.shortcut and shortcut comparison) so the
unregister/register cycle only runs when the togglePopupTerminal shortcut
actually changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 7583-7624: installCarbonHotKeyHandler currently sets
Self.carbonHandlerInstalled = true before calling InstallEventHandler, which
means a failed InstallEventHandler will permanently mark the handler installed;
change the flow so you call InstallEventHandler first, capture its returned
OSStatus (and any EventHandlerRef if used), check status == noErr, and only then
set Self.carbonHandlerInstalled = true; ensure you return or log the error and
leave the flag false when InstallEventHandler fails (referencing
installCarbonHotKeyHandler, Self.carbonHandlerInstalled, and InstallEventHandler
to locate the code).

---

Nitpick comments:
In `@Sources/cmuxApp.swift`:
- Around line 3507-3512: The current observer unconditionally calls
AppDelegate.shared?.registerPopupTerminalHotKey() on any UserDefaults change;
instead add a `@State` property (e.g., popupShortcut) to cache
KeyboardShortcutSettings.shortcut(for: .togglePopupTerminal) and in the
.onReceive(NotificationCenter.default.publisher(for:
UserDefaults.didChangeNotification)) handler read the latest via
KeyboardShortcutSettings.shortcut(for: .togglePopupTerminal) and only call
registerPopupTerminalHotKey() (and update popupShortcut) when the latest !=
popupShortcut; this mirrors the existing pattern used elsewhere (lines using
KeyboardShortcutSettings.shortcut and shortcut comparison) so the
unregister/register cycle only runs when the togglePopupTerminal shortcut
actually changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bcf7dacc-4a73-43c9-82f0-aa9b828b1f84

📥 Commits

Reviewing files that changed from the base of the PR and between b38256c and dcfcea9.

📒 Files selected for processing (6)
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift
  • Sources/PopupTerminalController.swift
  • Sources/TerminalController.swift
  • Sources/cmuxApp.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Sources/TerminalController.swift
  • CLI/cmux.swift
  • Sources/PopupTerminalController.swift

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

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/AppDelegate.swift">

<violation number="1" location="Sources/AppDelegate.swift:7546">
P2: Non-function popup shortcuts can be recorded and saved, but this guard silently skips registration so the popup shortcut stops working.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/AppDelegate.swift Outdated

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

♻️ Duplicate comments (1)
Sources/AppDelegate.swift (1)

7583-7624: ⚠️ Potential issue | 🟠 Major

Gate carbonHandlerInstalled on successful handler installation.

Line 7585 flips carbonHandlerInstalled before checking InstallEventHandler success. If install fails once, future retries are blocked and hotkey dispatch stays broken.

💡 Suggested fix
 private static var carbonHandlerInstalled = false
+private static var carbonHandlerRef: EventHandlerRef?

 private func installCarbonHotKeyHandler() {
     guard !Self.carbonHandlerInstalled else { return }
-    Self.carbonHandlerInstalled = true

     var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
-    InstallEventHandler(
+    var handlerRef: EventHandlerRef?
+    let status = InstallEventHandler(
         GetApplicationEventTarget(),
         { _, event, _ -> OSStatus in
             var hotKeyID = EventHotKeyID()
             let status = GetEventParameter(
                 event,
@@
             1,
             &eventType,
             nil,
-            nil
+            &handlerRef
         )
+    guard status == noErr else {
+#if DEBUG
+        dlog("popupTerminal.hotkey installHandlerFailed status=\(status)")
+#endif
+        return
+    }
+    Self.carbonHandlerRef = handlerRef
+    Self.carbonHandlerInstalled = true
 }
#!/bin/bash
# Verify the install flow currently sets the installed flag before checking Carbon status.
rg -n -C6 'private func installCarbonHotKeyHandler|carbonHandlerInstalled|InstallEventHandler\(' Sources/AppDelegate.swift
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 7583 - 7624, The code sets
Self.carbonHandlerInstalled = true at the start of installCarbonHotKeyHandler(),
which marks the handler installed even if InstallEventHandler fails; change the
flow so you only set Self.carbonHandlerInstalled = true after a successful
InstallEventHandler call (i.e., check InstallEventHandler's return/OSStatus and
only flip Self.carbonHandlerInstalled on success), leaving it false on failure
so future retries can attempt installation; locate the logic in
installCarbonHotKeyHandler() around the InstallEventHandler call and adjust
accordingly.
🤖 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/AppDelegate.swift`:
- Around line 9389-9391: The reverse key-code map currently maps "\r" to 36
which loses keypad-enter (76) when round-tripping; update the reverse mapping
entry for "\r" (the dictionary that currently contains "\"\\r\": 36") to
include/return the keypad-enter key code 76 (e.g., change the mapping so "\r"
corresponds to 76 or otherwise prefer 76 when choosing between 36 and 76) so
that storedKey(from:) (which normalizes 36 and 76 to "\r") round-trips correctly
to keypad-enter.

In `@Sources/cmuxApp.swift`:
- Around line 3480-3492: The popup width/height TextField bindings (popupWidth
and popupHeight used in the SettingsCardRow blocks and TextField views)
currently accept unrestricted numbers; add input-time clamping to enforce 10–100
so invalid values aren't persisted. Implement an onChange (or a custom Binding)
for each TextField (the ones bound to popupWidth and popupHeight) that clamps
the value to the 10–100 range and writes the clamped value back to the state
variable, ensuring the UI and persisted settings always remain within bounds.

---

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 7583-7624: The code sets Self.carbonHandlerInstalled = true at the
start of installCarbonHotKeyHandler(), which marks the handler installed even if
InstallEventHandler fails; change the flow so you only set
Self.carbonHandlerInstalled = true after a successful InstallEventHandler call
(i.e., check InstallEventHandler's return/OSStatus and only flip
Self.carbonHandlerInstalled on success), leaving it false on failure so future
retries can attempt installation; locate the logic in
installCarbonHotKeyHandler() around the InstallEventHandler call and adjust
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 476eb668-304c-4fc0-a382-7e820e5002ad

📥 Commits

Reviewing files that changed from the base of the PR and between dcfcea9 and 03230a2.

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

Comment thread Sources/AppDelegate.swift
Comment on lines +9389 to +9391
"\t": 48, "\r": 36,
"[": 33, "]": 30, "-": 27, "=": 24,
",": 43, ".": 47, "/": 44, ";": 41, "'": 39, "`": 50, "\\": 42,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify normalization vs registration mapping for Return/Keypad Enter.
rg -n -C3 'case 36, 76: return "\\r"|case 36, 76: return "\r"' Sources/KeyboardShortcutSettings.swift
rg -n -C3 '"\\r": 36|"\\r": 36' Sources/AppDelegate.swift

Repository: manaflow-ai/cmux

Length of output: 811


Keypad-enter is missing from the reverse key-code mapping.

storedKey(from:) normalizes both Return (36) and Keypad Enter (76) to "\r", but the reverse map at line 9389 only registers "\r": 36. A shortcut recorded with keypad-enter will map back to Return's key code instead of Keypad Enter's.

Mapping gap
// Normalization (KeyboardShortcutSettings.swift:394)
case 36, 76: return "\r" // return, keypad enter

// Reverse mapping (AppDelegate.swift:9389)
"\r": 36,  // missing 76
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 9389 - 9391, The reverse key-code map
currently maps "\r" to 36 which loses keypad-enter (76) when round-tripping;
update the reverse mapping entry for "\r" (the dictionary that currently
contains "\"\\r\": 36") to include/return the keypad-enter key code 76 (e.g.,
change the mapping so "\r" corresponds to 76 or otherwise prefer 76 when
choosing between 36 and 76) so that storedKey(from:) (which normalizes 36 and 76
to "\r") round-trips correctly to keypad-enter.

Comment thread Sources/cmuxApp.swift
Comment on lines +3480 to +3492
SettingsCardRow(String(localized: "popupTerminal.settings.widthPercent", defaultValue: "Width %")) {
TextField("", value: $popupWidth, format: .number)
.frame(width: 60)
.textFieldStyle(.roundedBorder)
}

SettingsCardDivider()

SettingsCardRow(String(localized: "popupTerminal.settings.heightPercent", defaultValue: "Height %")) {
TextField("", value: $popupHeight, format: .number)
.frame(width: 60)
.textFieldStyle(.roundedBorder)
}

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

Enforce 10–100 bounds for popup width/height at input time.

Line 3481 and Line 3489 currently accept unrestricted numeric values. Clamping before repositioning prevents invalid persisted settings and keeps behavior predictable.

Suggested fix
-        .onChange(of: popupWidth) { _ in PopupTerminalController.shared.reposition() }
-        .onChange(of: popupHeight) { _ in PopupTerminalController.shared.reposition() }
+        .onChange(of: popupWidth) { _ in
+            popupWidth = min(max(popupWidth, 10), 100)
+            PopupTerminalController.shared.reposition()
+        }
+        .onChange(of: popupHeight) { _ in
+            popupHeight = min(max(popupHeight, 10), 100)
+            PopupTerminalController.shared.reposition()
+        }

Also applies to: 3505-3506

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

In `@Sources/cmuxApp.swift` around lines 3480 - 3492, The popup width/height
TextField bindings (popupWidth and popupHeight used in the SettingsCardRow
blocks and TextField views) currently accept unrestricted numbers; add
input-time clamping to enforce 10–100 so invalid values aren't persisted.
Implement an onChange (or a custom Binding) for each TextField (the ones bound
to popupWidth and popupHeight) that clamps the value to the 10–100 range and
writes the clamped value back to the state variable, ensuring the UI and
persisted settings always remain within bounds.

@teamleaderleo teamleaderleo added the area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts label Sep 30, 2026

This branch has not been deployed

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

Labels

area: terminal Ghostty surface, rendering, scrollback, escape sequences, fonts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants