Skip to content

Menubar global search P1 - #3908

Merged
austinywang merged 23 commits into
mainfrom
issue-3865-menubar-global-search
May 13, 2026
Merged

austinywang merged 23 commits into
mainfrom
issue-3865-menubar-global-search

Conversation

@austinywang

@austinywang austinywang commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • add a local SQLite/FTS5 search index for live cmux browser, markdown, and title documents
  • add a menubar-anchored SwiftUI search palette with result navigation and browser inline find reuse
  • wire the default Option+Command+F shortcut through KeyboardShortcutSettings, Settings, cmux.json schema/docs data, and localized strings

Scope

Validation

  • git diff --check
  • python3 -m json.tool Resources/Localizable.xcstrings
  • python3 -m json.tool web/data/cmux.schema.json
  • plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Local tests and tagged app launch were intentionally not run per task constraints; CI is the test gate before launch.

Closes #3865


Note

Medium Risk
Adds a new SQLite/FTS-backed indexing subsystem plus new system-wide hotkey routing and menubar interactions, which could impact performance, input handling, and panel lifecycle behavior if edge cases are missed.

Overview
Adds a new menubar “Global Search” palette that searches across open windows/workspaces/panels and lets users open results (including keyboard navigation and ⌘1–⌘9 quick-open), anchored to the status item.

Introduces a local SQLite FTS5 SearchIndex plus GlobalSearchCoordinator/capture manager to keep browser/markdown/title documents indexed, debounce live captures, and purge index entries when panels close or content becomes unavailable.

Wires a new remappable system-wide globalSearch hotkey (default ⌥⌘F) end-to-end: shortcut settings + conflict rules, Carbon hotkey controller support, menu item shortcut display, localization strings, web schema/docs updates, and new unit tests for indexing and shortcut behavior.

Reviewed by Cursor Bugbot for commit 4ba99e5. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Adds a menubar global search with a SwiftUI palette and a local SQLite FTS5 index that searches browser pages, markdown files, titles, and now lists currently open panels for quick access. Ships P1 for #3865 with a remappable system‑wide globalSearch shortcut (⌥⌘F), reliable activation, and refined capture ownership to prevent stale or duplicate work.

  • New Features

    • Local FTS5 index (SearchIndex) with upsert/purge/search and BM25 ranking/snippets; starts at app launch and is skipped under XCTest.
    • Coordinator + capture manager (GlobalSearchCoordinator + GlobalSearchPanelCaptureManager) that track window/workspace/panel; debounced captures for browser (on navigation) and markdown (on file updates); purge on close/missing.
    • Menubar palette (MenubarSearchPopover) with arrow/Enter and ⌘1–9 quick‑open; shows currently open panels for quick access; left‑click opens search, right/control‑click shows menu; adds “Search All Windows…”, refreshes index when opened, and reuses browser inline find.
    • Remappable system‑wide globalSearch via KeyboardShortcutSettings and SystemWideHotkeyController; schema/docs and localized labels updated.
  • Bug Fixes

    • Removed duplicate index refresh on open; canceled refresh on dismiss; canceled stale markdown/browser captures.
    • Purged stale/unavailable entries and preserved markdown title search when file reads fail.
    • Ensured remapped globalSearch isn’t suppressed by stale menu shortcuts; addressed review blockers from CI and follow‑ups.

Written for commit 4ba99e5. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Global search palette from the menu bar with configurable global shortcut (⌥⌘F by default); search across browser pages, markdown files, and window titles. Keyboard navigation plus ⌘1–⌘9 quick-open.
  • Improvements

    • Live background indexing, debounced capture for panels, and improved system-wide hotkey handling and presentation behavior (left-click opens search; right/control-click shows menu).
  • Localization

    • Added English and Japanese strings for the global search UI.
  • Tests

    • Added unit tests covering search index behavior and global shortcut validation.

Review Change Stack

Phase 1 needs browser and markdown value without terminal scrollback, so the index owns durable FTS5 upserts while AppDelegate keeps one navigation path from palette rows to focused panels. The global shortcut is wired through the existing shortcut settings instead of a standalone Carbon shim so Settings and cmux.json remain authoritative.

Constraint: Phase 1 excludes Ghostty terminal scrollback capture

Constraint: User required no local test execution and no reload before CI is green

Rejected: Hardcoded GlobalSearchHotkey shim | violates KeyboardShortcutSettings policy

Confidence: medium

Scope-risk: broad

Directive: Keep future terminal capture feeding SearchIndex documents through GlobalSearchCoordinator rather than adding another palette/navigation path

Tested: git diff --check; python3 -m json.tool Resources/Localizable.xcstrings; python3 -m json.tool web/data/cmux.schema.json; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Not-tested: Local unit/UI tests and tagged app launch per task constraints
@vercel

vercel Bot commented May 12, 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 May 13, 2026 5:50am
cmux-staging Building Building Preview, Comment May 13, 2026 5:50am

@coderabbitai

coderabbitai Bot commented May 12, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a menubar global search: SQLite-backed FTS index (SearchIndex actor), GlobalSearchCoordinator for live indexing, MenubarSearchPopover SwiftUI palette, per-action system-wide hotkeys and shortcut normalization, panel capture hooks, tests, localization, and Xcode project wiring.

Changes

Global Search Feature

Layer / File(s) Summary
Search data models and error types
Sources/Search/SearchIndex.swift
GlobalSearchKind, SearchIndexDocument with stable composite ID, SearchIndexHit, and SearchIndexError.
SQLite FTS5 search database implementation
Sources/Search/SearchIndex.swift
SearchIndex actor manages DB lifecycle with WAL/PRAGMA, creates chunks and FTS5 virtual table with synchronized triggers, provides upsert/delete/search operations, tokenizes queries into AND-wildcard MATCH queries, ranks via BM25, and includes low-level SQLite helpers.
Global search coordinator and AppDelegate integration
Sources/Search/GlobalSearchCoordinator.swift, Sources/AppDelegate.swift
GlobalSearchCoordinator singleton manages index lifecycle, live refresh, debounced browser capture, markdown capture, panel purging, JS page extraction, and includes AppDelegate helpers to enumerate contexts, resolve panel targets, and open/activate hits.
Panel capture hooks
Sources/Panels/BrowserPanel.swift, Sources/Panels/MarkdownPanel.swift
BrowserPanel and MarkdownPanel call coordinator to capture or purge indexed content on navigation, KVO updates, content load, and close.
Menubar search popover and palette UI
Sources/Search/MenubarSearchPopover.swift
MenubarSearchPopover and GlobalSearchPaletteView implement debounced async search, generation/cancellation, key-event monitor, keyboard navigation (↑/↓/Enter/Esc), ⌘1–⌘9 shortcuts, result row model and GlobalSearchResultRowView, and activation via coordinator.
Keyboard shortcut action & per-action hotkeys
Sources/KeyboardShortcutSettings.swift
Adds .globalSearch action with localized label and default system-wide shortcut; refactors normalization and system-wide conflict detection to be action-aware; treats .globalSearch like .showHideAllWindows in persistence normalization; refactors SystemWideHotkeyController to per-action registration with mapping of Carbon hotkeys to actions.
Menubar wiring and controller changes
Sources/App/MenuBarExtraController.swift, Sources/AppDelegate.swift
MenuBarExtraController adds global search menu item, routes status item left-click to invoke the palette via statusItemButtonAction, applies configured shortcut; AppDelegate starts coordinator, wires menubar toggle, excludes .globalSearch from chord-arming, and adds toggleGlobalSearchPaletteFromGlobalHotkey().
Localization, configuration schema, web data, and project
Resources/Localizable.xcstrings, web/data/cmux-shortcuts.ts, web/data/cmux.schema.json, GhosttyTabs.xcodeproj/project.pbxproj
Adds en/ja strings for search UI, updates web shortcuts to include globalSearch and unbind sendFeedback, extends JSON schema to allow globalSearch, and registers new source and test files in the Xcode project.
Search index and shortcut configuration tests
cmuxTests/SearchIndexTests.swift, cmuxTests/GlobalSearchShortcutSettingsTests.swift
SearchIndexTests validates upsert/search, replacement, panel-scoped deletion, legacy backfill, and limit handling. GlobalSearchShortcutSettingsTests validates .globalSearch default remappability, system-wide modifier requirements, reserved-conflict rejection, and file-store parsing using per-test temporary stores.

Sequence Diagram

sequenceDiagram
  participant StatusItem as NSStatusBarButton
  participant MenubarPopover as MenubarSearchPopover
  participant Coordinator as GlobalSearchCoordinator
  participant Index as SearchIndex
  participant App as AppDelegate

  StatusItem->>MenubarPopover: open / type query
  MenubarPopover->>Coordinator: search(query:)
  Coordinator->>Index: search(query:, limit:)
  Index-->>Coordinator: [SearchIndexHit...]
  Coordinator-->>MenubarPopover: results
  MenubarPopover->>Coordinator: activate(hit, query:)
  Coordinator->>App: openGlobalSearchHit(hit, query:)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"🐰 I hopped through code and found the trail,
A tiny index in SQLite's vale,
Menubar opens, results appear,
Cross-window search, quick and clear,
A nibble, a hop — find what you hail."


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error PR introduces 2 DispatchSourceTimer instances for debouncing and 2 fire-and-forget Tasks with real lifecycle that violate Swift concurrency modernization rules. Replace DispatchSourceTimer with Task.sleep for debouncing. Store/track fire-and-forget Tasks or make them short-lived without persistent state.
Cmux Swift @Concurrent ❌ Error @MainActor GlobalSearchCoordinator async methods lack @concurrent. search(), refreshLiveIndex(), browserPagePayload() perform I/O without proper annotations despite UI context calls. Add @concurrent to GlobalSearchCoordinator.search(), refreshLiveIndex(), browserPagePayload() to mark file-I/O-bound work leaving MainActor.
Cmux Architecture Rethink ❌ Error Markdown capture lacks debounce sleep, causing immediate FTS upserts per keystroke. Browser inline find passes untokenized query, leaving mismatches representable when terms are separated. Add Task.sleep debounce to markdown capture. Share FTS tokenizer with applyBrowserInlineSearch to normalize query before inline find. Establish single tokenization source.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Menubar global search P1' is concise and directly reflects the main feature addition, though it's somewhat generic by omitting key implementation details like SQLite/FTS5 indexing.
Linked Issues check ✅ Passed The PR successfully implements all primary P1 coding requirements from #3865: SQLite FTS5 index with upsert/delete/search, browser/markdown capture with debouncing, menubar palette UI with keyboard navigation and quick-open (⌘1–9), system-wide hotkey (⌥⌘F) via remappable settings, and result activation across windows/workspaces.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to P1 objectives: SearchIndex and GlobalSearchCoordinator (index/capture/refresh), MenubarSearchPopover (palette UI), KeyboardShortcutSettings (shortcut action), MenuBarExtraController (menubar wiring), and supporting panel/test changes. Terminal scrollback capture correctly deferred to P2.
Cmux Swift Actor Isolation ✅ Passed SearchIndex is a proper actor. Value types conform to Sendable. UI classes marked @MainActor. No implicit MainActor models or isolation violations. Existing debt unchanged.
Cmux Swift Blocking Runtime ✅ Passed No blocking/timing synchronization introduced. SearchIndex uses actor isolation; GlobalSearchCoordinator uses DispatchSourceTimer for debouncing (non-blocking).
Cmux No Hacky Sleeps ✅ Passed PR introduces no hacky sleeps or timers in non-Swift runtime code. Only TypeScript change is pure data. Swift sleeps covered separately.
Cmux Swift File And Package Boundaries ✅ Passed Three new files all meet rules: SearchIndex (460 lines, single responsibility), GlobalSearchCoordinator (552, Coordinator pattern), MenubarSearchPopover (387, UI). Existing file growth moderate.
Cmux Swift Logging ✅ Passed No logging violations detected. All debug logging is properly guarded with #if DEBUG. No inappropriate Loggers. Complies with swift-logging.md rules.
Cmux Swiftui State Layout ✅ Passed All new SwiftUI state uses @State pattern. LazyVStack rows receive immutable snapshots and closures. No GeometryReader layout changes. State mutations only in lifecycle callbacks.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds only NSPopover (MenubarSearchPopover) and NSMenuItem (globalSearchItem), both explicitly allowed by rule. Lint script passed with no violations.
Description check ✅ Passed PR description comprehensively covers all required sections: Summary (what changed and why), Testing (validation commands provided), Demo (not applicable for this change), Review Trigger, and Checklist items addressed.
✨ 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 issue-3865-menubar-global-search

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.

The PR branch needed the latest mainline CLI and project-file changes before CI iteration so checks run against the current integration surface.

Constraint: iterate-pr workflow requires merging the base branch before CI feedback work

Confidence: high

Scope-risk: narrow

Directive: Preserve this merge unless rebasing the PR branch intentionally

Tested: merge completed without conflicts

Not-tested: Local tests/build per task constraints
Comment thread Sources/Search/SearchIndex.swift
Comment thread Sources/Search/GlobalSearchCoordinator.swift
Cursor review pointed out that the menubar toggle and palette onAppear both refreshed the live index. Keeping the refresh in the palette lifecycle avoids resetting browser debounce tasks while still indexing each time the palette opens, and removing the unused workspace delete API keeps the storage surface honest.

Constraint: Review feedback came from PR #3908 after the first CI pass started

Rejected: Keep both refresh calls | causes avoidable debounce cancellation and slower browser result availability

Confidence: high

Scope-risk: narrow

Directive: Add workspace-level deletion only when a real workspace teardown caller is wired

Tested: git diff --check; python3 -m json.tool Resources/Localizable.xcstrings; python3 -m json.tool web/data/cmux.schema.json; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Not-tested: Local tests/build per task constraints
@greptile-apps

greptile-apps Bot commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a menubar-anchored global search palette backed by a local SQLite/FTS5 index (SearchIndex actor), a GlobalSearchCoordinator/GlobalSearchPanelCaptureManager pipeline that debounces captures via DispatchSourceTimer, and a remappable system-wide globalSearch hotkey (⌥⌘F default) wired through KeyboardShortcutSettings and SystemWideHotkeyController.

  • New search infrastructure: SearchIndex actor opens off-MainActor via Task.detached, schema uses content-table FTS5 triggers for BM25 ranking, and the coordinator correctly manages index lifecycle states (idle → opening → ready/failed → retry).
  • Capture pipeline: Browser and markdown panels are hooked at all content-change sites; the capture manager uses task-ID guarding to prevent stale upserts racing against purges; the file-unavailable path correctly routes through captureMarkdownPanel (not purgePanel) to preserve title searchability while the panel is open.
  • Key monitor gap: isSystemCommand handles ⌘Q/W/H/M/, but omits ⌘. (the standard macOS cancel shortcut), which the default branch silently swallows; and cancelSearchWork() does not bump searchGeneration, so a timer that already dispatched its handler to the main queue before dismissal can still create and run a search task.

Confidence Score: 4/5

Mostly safe to merge; one concrete gap in the key monitor means ⌘. (the macOS cancel shortcut) is silently discarded while the palette is open.

The SQLite/FTS5 pipeline, capture lifecycle, and hotkey routing are well-structured. The one concrete defect is in the key-event filter: isSystemCommand covers ⌘Q/W/H/M/, but not ⌘., so the standard macOS cancel shortcut is swallowed whenever the palette is visible. The generation-counter race in cancelSearchWork is a secondary quality gap (wasted SQLite work on a dismissed palette, no user-visible misbehavior).

Sources/Search/MenubarSearchPopover.swift — the isSystemCommand function and cancelSearchWork both need small fixes.

Important Files Changed

Filename Overview
Sources/Search/MenubarSearchPopover.swift New 421-line SwiftUI popover palette with DispatchSourceTimer-backed debounce and local event monitor; isSystemCommand is missing "." (⌘.), and cancelSearchWork() doesn't bump searchGeneration, allowing a stale timer-fired task to run after dismissal.
Sources/Search/SearchIndex.swift New SQLite/FTS5 actor with correct WAL mode, content-table triggers, BM25 ranking, and snippet extraction; open() correctly uses Task.detached to avoid blocking the main actor during initialization.
Sources/Search/GlobalSearchCoordinator.swift New @mainactor coordinator with well-managed index lifecycle states (idle/opening/ready/failed), task-ID-guarded purge tasks, and recovery from the failed state; delegates all capture work to GlobalSearchPanelCaptureManager.
Sources/Search/GlobalSearchPanelCaptureManager.swift New 284-line capture manager with DispatchSourceTimer debounce for browser and markdown panels, task-ID-guarded capture tasks that cancel before purge, and correct markdown unavailability handling that routes through purgeMarkdownDocument instead of purgePanel.
Sources/KeyboardShortcutSettings.swift Adds globalSearch action as a system-wide remappable shortcut (⌥⌘F default); extends systemWideConflictShortcut, reservedSystemWideHotkeyShortcuts, and isMenuBackedShortcutAction to exclude both system-wide actions from menu-backed shortcut suppression.
Sources/AppDelegate.swift Adds transient MenuBarExtraController fallback for global-hotkey-triggered search when the persistent controller is absent; syncMenuBarExtraVisibility cleanup conditioned on previousShouldInstall to avoid spurious transient controller removal.
Sources/Search/AppDelegate+GlobalSearch.swift New extension splitting panel-context discovery and hit-navigation logic out of GlobalSearchCoordinator into its own AppDelegate extension; correctly uses weak references and resolves panels through live contexts before falling back to recoverable routes.
Sources/Panels/MarkdownPanel.swift Correctly hooks all content-change paths into captureMarkdownPanel; unavailable-file path now calls captureMarkdownPanel (not purgePanel) so the panel title remains searchable while the panel is open.
Sources/App/MenuBarExtraController.swift Replaces the direct statusItem.menu = menu assignment with a custom action handler that distinguishes left-click (open search palette) from right/control-click (show menu); adds toggleGlobalSearchPalette and wires globalSearch shortcut to the menu item.
Sources/Search/GlobalSearchDocuments.swift New document-builder helpers for title, browser, and markdown index documents with text capping at 400k characters; browseHit synthesizes a non-indexed hit for the open-panel browse mode.

Sequence Diagram

sequenceDiagram
    participant HK as SystemWideHotkeyController
    participant AD as AppDelegate
    participant MBC as MenuBarExtraController
    participant GC as GlobalSearchCoordinator
    participant Pop as MenubarSearchPopover
    participant CM as GlobalSearchPanelCaptureManager
    participant SI as SearchIndex (actor)

    HK->>AD: perform(.globalSearch)
    AD->>MBC: toggleGlobalSearchPalette()
    MBC->>GC: togglePalette(anchor:)
    GC->>Pop: toggle(relativeTo:)
    Pop->>Pop: onAppear → installKeyMonitor + resetResults
    Pop->>GC: refreshLiveIndex()
    GC->>AD: globalSearchPanelContexts()
    GC->>SI: upsert(titleDocument)
    GC->>CM: refreshPanelContent(for:index:)
    CM->>SI: upsert(browserDocument / markdownDocument)

    Note over Pop: User types query
    Pop->>Pop: scheduleSearch() via DispatchSourceTimer (80ms)
    Pop->>GC: search(query:)
    GC->>SI: search(_:limit:)
    SI-->>GC: [SearchIndexHit]
    GC-->>Pop: results
    Pop->>Pop: render result rows

    Note over Pop: User selects result
    Pop->>GC: activate(hit:query:)
    GC->>Pop: dismiss()
    GC->>AD: openGlobalSearchHit(_:query:)
    AD->>AD: focusMainWindow + selectTab + focusSurface
    AD->>AD: applyBrowserInlineSearch / applyMarkdownInlineSearch

    Note over Pop: Panel closes
    BrowserPanel->>GC: purgePanel(id:)
    GC->>CM: cancelCaptures(forPanelID:)
    GC->>SI: deletePanel(_:)
Loading

Comments Outside Diff (1)

  1. Sources/Search/MenubarSearchPopover.swift, line 2136-2139 (link)

    P1 ⌘. (cancel shortcut) is missing from isSystemCommand and gets swallowed

    isSystemCommand handles ⌘H, ⌘M, ⌘Q, ⌘W, and ⌘, but not ⌘.. On macOS, ⌘. is the standard cancel/stop shortcut consumed by NSAlert, NSOpenPanel, NSProgressIndicator, and other system sheets. The default branch in handleKeyEvent returns !isTextEditingCommand(event) && !isSystemCommand(event) for all ⌘+key combos not caught earlier, which evaluates to true for ⌘. — permanently discarding the event. If a system dialog is shown while the palette is open (e.g. a save sheet triggered by another path), ⌘. will silently fail to cancel it.

Reviews (14): Last reviewed commit: "feat: show open panels in global search" | Re-trigger Greptile

Comment thread Sources/Search/GlobalSearchCoordinator.swift Outdated
Comment thread Sources/Search/GlobalSearchCoordinator.swift Outdated
Comment on lines +1 to +30
import AppKit
import Foundation

@MainActor
struct GlobalSearchPanelContext {
let windowID: UUID
let windowTitle: String
let workspaceID: UUID
let workspaceTitle: String
let panelID: UUID
let panelTitle: String
let panel: any Panel

var location: String {
"\(windowTitle) > \(workspaceTitle)"
}
}

@MainActor
final class GlobalSearchCoordinator {
static let shared = GlobalSearchCoordinator()

private let maxIndexedTextCharacters = 400_000
private let browserCaptureDebounceNanoseconds: UInt64 = 250_000_000
private var browserCaptureTasks: [UUID: Task<Void, Never>] = [:]
private var browserCaptureTaskIDs: [UUID: UUID] = [:]
private var index: SearchIndex?
private var indexCreationFailed = false
private lazy var popover = MenubarSearchPopover(coordinator: self)

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.

P2 Mixed responsibilities and AppDelegate navigation logic in coordinator file

GlobalSearchCoordinator.swift (398 lines) bundles: index lifecycle (ensureIndex, SearchIndex upsert/purge), browser JS payload extraction (browserPagePayload), markdown and title document building, and a large AppDelegate extension that owns panel-context discovery (globalSearchPanelContexts, globalSearchContext) and hit navigation (openGlobalSearchHit, applyBrowserInlineSearch). The AppDelegate extension in particular wires window/workspace focus and browser inline-find reuse — that is UI navigation logic, not search orchestration. Splitting the AppDelegate extension into AppDelegate+GlobalSearch.swift and keeping GlobalSearchCoordinator to index lifecycle + document builders would make the boundary explicit and keep the file under the 400-line policy threshold.

Rule Used: Flag Swift changes that add too much unrelated res... (source)

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

Comment on lines +1 to +20
import Foundation
import SQLite3

enum GlobalSearchKind: String, Codable, Sendable {
case browser
case markdown
case title

var localizedLabel: String {
switch self {
case .browser:
return String(localized: "globalSearch.kind.browser", defaultValue: "Browser")
case .markdown:
return String(localized: "globalSearch.kind.markdown", defaultValue: "Markdown")
case .title:
return String(localized: "globalSearch.kind.title", defaultValue: "Title")
}
}
}

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.

P2 New 410-line file crosses the 400-line threshold; SearchIndex meets the package-boundary signal

SearchIndex.swift at 410 lines is a single coherent SQLite actor with no AppKit/SwiftUI/Ghostty dependencies — all four package-boundary signals apply: stable domain noun and public API, can be tested without launching cmux, owns a persistence schema, and would be safer behind a protocol. Extracting it (plus SearchIndexDocument, SearchIndexHit, GlobalSearchKind, SearchIndexError) into a cmux-search-index SwiftPM target would let tests run without app launch scaffolding and bring the file under budget.

Rule Used: Flag Swift changes that add too much unrelated res... (source)

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

coderabbitai[bot]
coderabbitai Bot previously requested changes May 12, 2026

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Search/GlobalSearchCoordinator.swift`:
- Around line 185-199: The SearchIndexDocument currently uses
SearchIndexDocument.stableID(windowID:workspaceID:panelID:kind:anchor:) which
ties the index key to mutable fields (windowID, workspaceID, anchor) so updates
create duplicate rows; change the ID generation to a panel-stable key composed
of panelID + kind (and a fixed subtype when needed) — e.g. replace calls to
SearchIndexDocument.stableID(...) in the document construction sites (the
builder in GlobalSearchCoordinator and the other affected blocks around the
indicated ranges) with a new/stable ID factory that uses only panelID and kind
(and subtype if required), and leave windowID, workspaceID, location/anchor as
updatable payload fields on SearchIndexDocument so moves/navigation update the
existing row instead of inserting a new one.

In `@Sources/Search/MenubarSearchPopover.swift`:
- Around line 151-195: When the popover is open, unhandled Command-key shortcuts
should be swallowed so they don't propagate to the app; update handleKeyEvent
(and ensure installKeyMonitorIfNeeded/removeKeyMonitor keep using it) to first
check whether the palette/popover is visible and, if so, consume any
Command-modified events that are not in a small allowlist. Keep the existing
allowlist (numeric quick-open via charactersIgnoringModifiers -> openResult,
navigation/up/down handling via keyCodes 126/125, Escape 53 ->
coordinator.dismissPalette, Enter/Return 36/76 -> openSelectedResult) but for
other Command combinations (including zoom shortcuts like Command+'=',
Command+'-', Command+'0' and other characters) return true to swallow them;
implement the visibility check using your popover visibility predicate and
ensure keyMonitor still returns nil when events are consumed.

In `@Sources/Search/SearchIndex.swift`:
- Around line 371-381: The function matchQuery(for:) currently builds FTS5
queries from tokens but preserves original casing, causing uppercase tokens like
"AND"/"OR"/"NOT"/"NEAR" to collide with FTS5 operators (e.g., generating invalid
"AND*"); update matchQuery to lowercase each token (use tokens.map {
$0.lowercased() } or similar) before appending the prefix modifier and joining
so that reserved operator words are not produced in uppercase and the final
expression is valid for FTS5; keep the rest of the logic (trimming, filtering,
appending "*" and joining with " AND ") unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1e0b9120-51b3-4c8f-9270-b3e5868bd647

📥 Commits

Reviewing files that changed from the base of the PR and between 31a99c2 and bfe8fcd.

📒 Files selected for processing (14)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/Localizable.xcstrings
  • Sources/App/MenuBarExtraController.swift
  • Sources/AppDelegate.swift
  • Sources/KeyboardShortcutSettings.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/MarkdownPanel.swift
  • Sources/Search/GlobalSearchCoordinator.swift
  • Sources/Search/MenubarSearchPopover.swift
  • Sources/Search/SearchIndex.swift
  • cmuxTests/GlobalSearchShortcutSettingsTests.swift
  • cmuxTests/SearchIndexTests.swift
  • web/data/cmux-shortcuts.ts
  • web/data/cmux.schema.json

Comment thread Sources/Search/GlobalSearchCoordinator.swift Outdated
Comment thread Sources/Search/MenubarSearchPopover.swift
Comment thread Sources/Search/SearchIndex.swift Outdated
Comment thread Sources/Search/SearchIndex.swift
Comment thread Sources/Search/GlobalSearchCoordinator.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 12, 2026

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

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

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 7187-7193: The guard early-returns in
toggleGlobalSearchPaletteFromGlobalHotkey() when menuBarExtraController is nil,
causing the global-hotkey path to silently fail; instead remove the
NSSound.beep()/return and implement a fallback that ensures the palette is shown
(either by creating/initializing menuBarExtraController then calling
menuBarExtraController.toggleGlobalSearchPalette(), or by routing to an
alternative presentation method such as a shared App-level function that
presents the global search palette). Update
toggleGlobalSearchPaletteFromGlobalHotkey() to attempt to recover from a nil
menuBarExtraController before giving up so the global hotkey always opens the
palette.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5f518894-9e7b-4ebf-b762-551c119a8197

📥 Commits

Reviewing files that changed from the base of the PR and between bfe8fcd and a8cc253.

📒 Files selected for processing (2)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Sources/AppDelegate.swift

Comment thread Sources/AppDelegate.swift
Comment thread Sources/KeyboardShortcutSettings.swift
Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/Panels/MarkdownPanel.swift Outdated
Comment thread Sources/Search/GlobalSearchCoordinator.swift Outdated
Comment thread Sources/Search/GlobalSearchCoordinator.swift
Comment thread Sources/Search/MenubarSearchPopover.swift
Comment thread Sources/Search/SearchIndex.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes May 12, 2026

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

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

Inline comments:
In `@Sources/AppDelegate.swift`:
- Line 1157: The call to GlobalSearchCoordinator.shared.start() should be
skipped when running under XCTest to avoid initializing the SQLite/FTS stack for
tests; update the AppDelegate startup (where
GlobalSearchCoordinator.shared.start() is invoked) to early-return or
conditionally skip that call when
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] is present
(or another existing test-detection flag used in the project), so replace the
unconditional GlobalSearchCoordinator.shared.start() with a guard that detects
XCTest and only starts the coordinator when not in a test run.
- Around line 7310-7334: The method currently always removes
transientGlobalSearchMenuBarExtraController before toggling, preventing the
existing transient palette from being closed by a second hotkey; instead, if
transientGlobalSearchMenuBarExtraController already exists, call its
toggleGlobalSearchPalette(onDismiss:) and return that result (removing and
nil-ing only if toggle reports failure), otherwise create a new controller via
makeMenuBarExtraController and call toggleGlobalSearchPalette(onDismiss:) as
before; update references to transientGlobalSearchMenuBarExtraController,
makeMenuBarExtraController, toggleGlobalSearchPalette(onDismiss:), and
removeFromMenuBar accordingly so you no longer unconditionally remove the
existing controller at the start.

In `@Sources/Search/GlobalSearchCoordinator.swift`:
- Around line 20-33: GlobalSearchCoordinator currently mixes index lifecycle, UI
popover ownership, browser/markdown capture/parsing, and AppDelegate bridge
code; extract the capture/parsing responsibilities and AppDelegate bridge into
separate types/files: create a new PanelCaptureManager (or similar) to own
browserCaptureTimers, browserCaptureTasks, browserCaptureTaskIDs,
markdownCaptureTasks, markdownCaptureTaskIDs and all browser extraction/JSON
parsing methods, and move AppDelegate-related navigation helpers into an
AppDelegate+SearchCoordinator extension file; keep GlobalSearchCoordinator
focused on index lifecycle (startupIndexTask, index, indexCreationFailed) and
popover ownership (popover, MenubarSearchPopover), inject the new
PanelCaptureManager into GlobalSearchCoordinator, update call sites to use the
new manager, and ensure access control and tests are updated accordingly.
- Around line 468-483: openGlobalSearchHit currently only applies the query to
BrowserPanel via applyBrowserInlineSearch/BrowserSearchState, so markdown-buffer
hits only focus the panel without highlighting/jumping to the match; update
openGlobalSearchHit (or the panel-focus branch) to also apply the same inline
search behavior to markdown/document panels by reusing the search logic: detect
markdown/document panels (the same check used to find browserPanel or via panel
type) and set their search state or invoke the equivalent search/jump method
with the trimmed needle (use BrowserSearchState(needle:) or the document panel’s
search API) so the query is applied for both BrowserPanel and markdown buffers.

In `@Sources/Search/SearchIndex.swift`:
- Around line 244-294: The DB initialization in configureDatabase(_:
OpaquePointer) must track a schema version (PRAGMA user_version) and, when
upgrading from a prior version that lacked a populated FTS index, rebuild or
repopulate the external-content FTS table chunks_fts so existing rows in chunks
are indexed; implement: read PRAGMA user_version, if it is < targetVersion then
after creating the chunks_fts table and triggers call the proper FTS5 rebuild
command (e.g. execute an FTS5 rebuild like INSERT INTO chunks_fts(chunks_fts)
VALUES('rebuild') or explicitly repopulate via INSERT INTO chunks_fts(rowid,
title, location, text) SELECT rowid, title, location, text FROM chunks), then
set PRAGMA user_version = targetVersion; update configureDatabase and reuse the
existing execute(...) helper and keep the chunks_ai/chunks_ad/chunks_au triggers
as-is.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e6a5e67-b9f3-4dda-ad70-2b81752b87a5

📥 Commits

Reviewing files that changed from the base of the PR and between a8cc253 and 84aa446.

📒 Files selected for processing (8)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/Localizable.xcstrings
  • Sources/App/MenuBarExtraController.swift
  • Sources/AppDelegate.swift
  • Sources/Search/GlobalSearchCoordinator.swift
  • Sources/Search/MenubarSearchPopover.swift
  • Sources/Search/SearchIndex.swift
  • cmuxTests/SearchIndexTests.swift

Comment thread Sources/AppDelegate.swift Outdated
Comment thread Sources/AppDelegate.swift
Comment thread Sources/Search/GlobalSearchCoordinator.swift
Comment thread Sources/Search/GlobalSearchCoordinator.swift Outdated
Comment thread Sources/Search/SearchIndex.swift
Comment thread Sources/AppDelegate.swift
Comment thread Sources/Search/GlobalSearchPanelCaptureManager.swift Outdated
Comment on lines +245 to +252
default:
if flags.contains(.command),
!flags.contains(.option),
!flags.contains(.control) {
return !isTextEditingCommand(event)
}
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Palette key monitor swallows ⌘Q, ⌘W, ⌘H, and other system shortcuts

The default branch returns !isTextEditingCommand(event) for any ⌘-keyed event that isn't ⌘1–9. isTextEditingCommand only returns true for ⌘A/C/V/X/Z and delete/arrow key codes. Every other ⌘+key combination — including ⌘Q (quit), ⌘W (close window), ⌘M (minimize), ⌘H (hide app) — returns false from isTextEditingCommand, so handleKeyEvent returns true and the local event monitor returns nil, permanently discarding the event before it reaches the menu system. A user who opens the palette and presses ⌘Q to quit the app will find nothing happens; the palette stays open and the quit action is silently lost. The ⌘1–9 quick-open shortcuts are already handled by the explicit guard above the switch, so the default branch can safely return false for all remaining inputs.

Suggested change
default:
if flags.contains(.command),
!flags.contains(.option),
!flags.contains(.control) {
return !isTextEditingCommand(event)
}
return false
}
default:
return false

@lawrencecchen
lawrencecchen dismissed stale reviews from coderabbitai[bot], coderabbitai[bot], coderabbitai[bot], coderabbitai[bot], and coderabbitai[bot] May 13, 2026 05:22

Stale CodeRabbit review from an earlier commit range. The actionable findings were addressed in later commits, and the latest CodeRabbit run on the current head is non-blocking.

Comment thread Sources/Search/AppDelegate+GlobalSearch.swift
let text = GlobalSearchDocuments.cappedText([title, location, bodyText].filter { !$0.isEmpty }.joined(separator: "\n"))
guard !text.isEmpty else { return }

let anchor = GlobalSearchDocuments.firstNonEmpty(location, panel.id.uuidString) ?? panel.id.uuidString

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dead fallback in firstNonEmpty anchor construction

Low Severity

The expression GlobalSearchDocuments.firstNonEmpty(location, panel.id.uuidString) ?? panel.id.uuidString has a dead ?? panel.id.uuidString fallback. Since panel.id.uuidString is always non-empty and is passed as the second argument to firstNonEmpty, the function can never return nil here — the UUID string will always be selected if location is empty.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5c9ff7e. Configure here.

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4ba99e5. Configure here.

@State private var query = ""
@State private var results: [GlobalSearchResultRow] = []
@State private var selectedIndex = 0
@State private var isSearching = 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.

Empty state shows misleading "No results" during active search

Medium Severity

The isSearching state is tracked throughout the search lifecycle but never read in the view body. This means when the user types a query and the debounced search is in-flight, the empty state displays "No results" instead of a loading indicator or the intended "Type to search" prompt. Additionally, the localized string globalSearch.empty.prompt ("Type to search") was added to Localizable.xcstrings but is never referenced anywhere in code, suggesting the intent was to use isSearching to differentiate between "still searching" and "search complete with no matches."

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ba99e5. Configure here.

"en": { "stringUnit": { "state": "translated", "value": "Type to search" } },
"ja": { "stringUnit": { "state": "translated", "value": "入力して検索" } }
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Localized string globalSearch.empty.prompt defined but never used

Low Severity

The localized string globalSearch.empty.prompt ("Type to search" / "入力して検索") is defined in the strings catalog but is never referenced anywhere in source code. This appears to be the intended empty state text for the search palette before the user types anything, but it was never wired into GlobalSearchPaletteView — which instead shows "No open panels" for the empty-query state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4ba99e5. Configure here.

@austinywang
austinywang merged commit 2a42f80 into main May 13, 2026
27 checks passed
lawrencecchen added a commit that referenced this pull request May 14, 2026
* feat: improve markdown viewer

* fix: address markdown review feedback

* fix: clean up markdown review issues

* fix: address markdown review feedback

* fix: address latest markdown review

* Fix #3807: bring notification CLI to panel parity (#3811)

* Prove notification CLI parity is missing

Add behavior-level coverage for the missing notification socket and CLI actions before implementing them. The tests drive V2 notification action methods, CLI subcommands, extended list fields, and the UI open-notification flow so CI can show the pre-fix gap.

Constraint: Regression tests must be committed before the implementation for issue #3807

Constraint: Local Swift tests are not run in this repo; verification is through CI

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: Swift/XCUITest execution; intentionally deferred to CI

* Make notification actions scriptable from the CLI

The notifications panel already owned the behavior for dismissing, marking read, opening, and jumping to unread rows. This wires the socket and CLI to those existing store/AppDelegate paths, extends list output with panel metadata, and documents the new command surface without introducing a second notification action model.

Constraint: Existing Notifications page behavior must remain the source of truth

Constraint: Direct xcodebuild and local Swift/XCUITest runs are forbidden in this workspace

Rejected: Duplicate CLI-side focus or read-state logic | would diverge from AppDelegate.openNotification and TerminalNotificationStore semantics

Confidence: medium

Scope-risk: moderate

Tested: git diff --check

Tested: jq empty Resources/Localizable.xcstrings

Tested: python3 -m json.tool Resources/Localizable.xcstrings

Not-tested: Swift unit/UI execution; deferred to CI per repo policy

* Make notification CI compile under strict concurrency

The notification parity implementation introduced a shared formatter on a main-actor type and a test helper that crossed actor boundaries through escaping closures. This keeps the socket behavior unchanged while making date formatting local to the main-actor call path and keeping the XCTest socket request helper from capturing actor-isolated state in the background request closure.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace

Rejected: Run xcodebuild locally to confirm | user explicitly warned direct xcodebuild can deadlock the machine

Confidence: medium

Scope-risk: narrow

Tested: git diff --check

Tested: jq empty Resources/Localizable.xcstrings

Not-tested: Swift compilation and XCTest execution; deferred to CI

* Make notification parity actions atomic and exact

Review feedback exposed two behavior risks in the notification parity path: bulk dismissal was implemented as client-side list-and-loop work, and jump-to-unread could report one unread notification while opening a later valid one. The server now owns already-read dismissal as a single V2 action, the jump helper returns the notification it actually opened, and event/list parsing details match those server semantics.

Constraint: Notification action logic must reuse the existing store and AppDelegate paths

Rejected: Split TerminalController into new controllers in this PR | broad refactor is unrelated to issue #3807 and would obscure the parity fix

Confidence: medium

Scope-risk: moderate

Tested: git diff --check

Tested: jq empty Resources/Localizable.xcstrings

Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace instructions

* Expose app test symbols to CLI notification coverage

The integration regression exercises real app-backed notification state through the CLI harness, so the test target must import the built app module the same way the socket action tests do.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace
Rejected: Verify with xcodebuild locally | user explicitly forbids direct xcodebuild
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI

* Make notification list and dismiss payloads stable

Review caught two small contract hazards in the new notification RPC surface: the dismiss result used mixed JSON types, and a pipe in the appended tab title could shift the legacy list parser. The server now reports dismissals as counts consistently and escapes only the new trailing list field, with the CLI decoding it after structural parsing.

Constraint: The V1 list response remains pipe-delimited for backward compatibility, so only newly appended trailing fields can be encoded without changing old parser behavior
Rejected: Replace list_notifications with JSON-only output | existing V1 parsers depend on the line format
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace policy

* Return async notification socket responses from tests

CircleCI caught a compile-only issue in the async V2 test helper: the continuation result was awaited but not returned from the method that promises a response dictionary. Returning the continuation value restores the helper contract without changing the exercised socket behavior.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace
Rejected: Verify with local xcodebuild | user explicitly forbids direct xcodebuild
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI

* Avoid blocking notification CLI integration sockets

The integration regression has to create AppKit-backed notification state on the main actor, but running the CLI subprocess synchronously there can block the socket handler's main-actor store mutations. The test now awaits subprocess work on a background queue and verifies read state through the same CLI list path.

Constraint: Socket notification handlers intentionally hop to the main actor for store and AppDelegate work
Rejected: Run the CLI synchronously from the main actor | it can deadlock the in-process socket server during tests
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace policy

* Harden notification CLI response contracts

The notification parity path now distinguishes new list-notification trailer fields from old pipe-heavy bodies, rejects surface-only mark-read selectors, and returns post-open read state by marking through the shared store path after a successful focus action.

Constraint: list_notifications remains a legacy pipe-delimited protocol, so the new trailer needs its own discriminator while older body parsing keeps joining payload[6...]
Rejected: Add a third list_notifications sentinel field | the issue asked for exactly the two appended fields
Rejected: Rely on AppDelegate delayed mark-read for socket JSON | CLI callers need the open response and subsequent list output to reflect the explicit action
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace policy

* Make CLI integration helper capture explicit self

CircleCI's Swift compile step requires explicit self when the background subprocess closure calls the test helper method. This keeps the deadlock fix intact while satisfying Swift capture rules.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace
Rejected: Verify with local xcodebuild | user explicitly forbids direct xcodebuild
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI

* Make CLI presentation flags order-independent

Users naturally put presentation flags next to the command they are inspecting. Normalize --json and --id-format after command selection so docs examples such as cmux list-notifications --json produce JSON while still preserving literal flag-looking values for command options.

Constraint: Do not run reload.sh before CI is green; never run bare xcodebuild.

Rejected: Documentation-only correction | leaves copy-pasted command behavior surprising.

Rejected: Notification-only --json handling | repeats the same parser split for future JSON-capable commands.

Confidence: medium

Scope-risk: moderate

Directive: Keep presentation flag parsing centralized; update commandOptionsWithValues when adding value-taking command options.

Tested: git diff --check

Not-tested: Local XCTest/build per repo policy; CI will verify.

* Fix notification surface selector error message

* Make notification open mark read synchronously

* Fix CLI presentation flag parsing

* Test notification mark-read missing id

* Reject missing notification mark-read ids

* fix: polish markdown viewer dogfood

* Fix shared WebView task manager attribution

Fixes shared WebContent resource attribution in task manager rows and adds regression coverage.

* Prevent display-link crash from terminal portal layout reentry (#3885)

* Pin portal sync deferral during SwiftUI host callbacks

The silent-exit crash path points at SwiftUI/AppKit layout recursion reaching a CATransaction display-link flush. This test locks the intended invariant: geometry callbacks originating from the SwiftUI NSViewRepresentable host must defer portal reconciliation instead of forcing immediate AppKit layout, even while an interactive resize is active.

Constraint: Local tests are intentionally not run in this repository; CI owns regression proof.\nRejected: Assert on source text or unified-log contents | timing-dependent and not executable through the policy seam.\nConfidence: medium\nScope-risk: narrow\nDirective: Keep immediate portal flushing owned by external AppKit resize observers, not SwiftUI host callbacks.\nTested: Not run locally per repository policy and user instruction.\nNot-tested: Full multi-session crash reproduction is timing-dependent and not deterministic.

* Prevent portal layout reentry from terminal host callbacks

The terminal NSViewRepresentable host was allowed to bind into the window portal and synchronously flush AppKit layout from update/layout callbacks. That made SwiftUI's own host layout and the external terminal portal both believe they could drive geometry in the same render turn, matching the NSHostingView reentrant-layout warnings reported before the CATransaction display-link abort.\n\nThe portal now treats SwiftUI host callbacks as state capture only: they register the binding and schedule the portal owner to reconcile geometry after the current render turn. Immediate geometry flushing remains available to the portal's external AppKit resize observers, which are outside SwiftUI body/layout evaluation. The launch path also records clean exits and posts a one-time TerminalNotificationStore breadcrumb when a newer ghostty Breakpad envelope is found after an unclean exit.

Constraint: Local tests and app builds are not run before CI for this branch; verification is delegated to CI, then the required tagged reload.\nRejected: Wrap the CATransaction/display-link exception in @try/@catch | it would hide AppKit's abort symptom without removing the reentrant layout owner split.\nRejected: Keep immediate sync during interactive SwiftUI host callbacks | still allows layoutSubtreeIfNeeded inside the representable layout/update path.\nConfidence: medium\nScope-risk: moderate\nDirective: SwiftUI/AppKit host callbacks must not force terminal portal layout synchronously; add external observer paths for any future immediate resize flushing.\nTested: git diff --check; jq empty Resources/Localizable.xcstrings\nNot-tested: Local XCTest/build/repro per repository and user instructions; 75-minute multi-session crash reproduction is not deterministic.

* Record clean-exit breadcrumb after teardown

The crash breadcrumb should compare Breakpad envelopes against the last successfully completed termination path. Recording the timestamp after teardown avoids marking an exit clean before session persistence, process cleanup, and notification cleanup have finished.

Constraint: This is a follow-up correctness tweak before CI settled.\nRejected: Keep the timestamp at the start of applicationWillTerminate | a crash during termination cleanup could suppress the next-launch breadcrumb.\nConfidence: high\nScope-risk: narrow\nDirective: Only update the clean-exit timestamp after teardown work that must complete for a clean quit.\nTested: git diff --check; jq empty Resources/Localizable.xcstrings\nNot-tested: Local tests/builds per repository and user instructions.

* Defer first portal install from SwiftUI callbacks

A deferred SwiftUI host bind could still create the WindowTerminalPortal for the first time, and the initializer previously installed the host with an immediate layout flush. Threading the same deferral flag through portal creation keeps the invariant complete: representable update/layout callbacks never synchronously flush terminal portal layout, even on first bind.

Constraint: Must preserve immediate install behavior for non-SwiftUI external portal callers
Rejected: Assume portals already exist before host callbacks | first terminal bind in a new window can create one
Confidence: high
Scope-risk: narrow
Directive: Any future portal creation path from SwiftUI callbacks must carry deferred synchronization through initialization
Tested: git diff --check; jq empty Resources/Localizable.xcstrings
Not-tested: Local tests/builds per repository and user instructions

* Move crash breadcrumb work out of launch hot path

The crash breadcrumb scanner is pure Foundation logic, so it now lives in its own source file and performs the crash-directory scan from a detached utility task. App launch only schedules the check once, then posts the existing localized notification after the background scan returns to the main actor. The terminal host geometry policy is also simplified to make the always-deferred invariant explicit at the call site.

Constraint: Local build/tests are intentionally skipped until CI completes per issue instructions
Rejected: Keep synchronous directory enumeration in AppDelegate.configure | startup should not block on crash artifact I/O
Rejected: Preserve the dead immediate portal sync branch | host callbacks are never allowed to force layout synchronously
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce synchronous crash-directory scans or immediate portal layout flushes from SwiftUI host callbacks
Tested: git diff --check; jq empty Resources/Localizable.xcstrings; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj
Not-tested: Local build/tests per repository and user instructions

* Tighten portal geometry regression seam

* Fix crash breadcrumb actor isolation

* Mark crash breadcrumb async helper nonisolated

* Make crash breadcrumb scan concurrent

* fix: remove inert crash breadcrumb cooldown key

* fix: use renamed crash breadcrumb annotation

* fix: own crash breadcrumb scan task

* Hide sidebar descriptions in title-only mode (#4040)

* Hide sidebar descriptions in title-only mode

* Add sidebar description visibility toggle

* Let sidebar titles use trailing slack

* Float sidebar shortcut hints over rows

* Remove unused sidebar width helper

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add Pi agent icon

PR: https://github.com/manaflow-ai/cmux/pull/4057

* Keep Claude Running after clear (#3631)

* Prove Claude clear hook loses running status

The regression exercises the Claude hook handler directly with a SessionStart payload whose source is clear, then sends a late Stop for the previous session. The current handler does not promote the clear boundary to Running or reject stale Stop mutations, so this test is expected to fail before the lifecycle fix.

Constraint: Tests must exercise the hook handler directly rather than driving the full app.

Confidence: high

Scope-risk: narrow

Directive: Keep this regression on the direct CLI hook path; do not replace it with source-text assertions.

Tested: Not run locally per repository policy.

Not-tested: CI red/green proof pending on GitHub Actions.

* Prove Claude clear should own running status

Add hook-level regression coverage for Claude Code /clear. The tests drive the bundled CLI against a mock cmux socket so CI proves SessionStart(source=clear) must set the visible Running status and a prior session Stop must not clobber that fresh lifecycle.

Constraint: Local tests are intentionally not run; CI owns test execution for this task.
Confidence: high
Scope-risk: narrow
Tested: Not run locally per instruction
Not-tested: Full app UI repro under CI

* Ignore stale Claude hook events after clear

Claude /clear starts a fresh hook lifecycle but the sidebar status key is shared at the workspace level. Persist the active Claude session per workspace, promote clear SessionStart to a visible Running state, and ignore teardown or notification mutations from sessions that are no longer current.

Constraint: Claude Code emits /clear as SessionStart(source=clear).
Rejected: Only set Running on clear SessionStart | late Stop from the previous lifecycle could still clobber the new status.
Confidence: high
Scope-risk: narrow
Directive: Future Claude hook status mutations must respect the active workspace session before touching claude_code.
Tested: Not run locally per instruction
Not-tested: Full app UI after CI

* Prove Claude clear hook loses running status

The regression exercises the Claude hook handler directly with a SessionStart payload whose source is clear, then sends a late Stop for the previous session. The current handler does not promote the clear boundary to Running or reject stale Stop mutations, so this test is expected to fail before the lifecycle fix.

Constraint: Tests must exercise the hook handler directly rather than driving the full app.

Confidence: high

Scope-risk: narrow

Directive: Keep this regression on the direct CLI hook path; do not replace it with source-text assertions.

Tested: Not run locally per repository policy.

Not-tested: CI red/green proof pending on GitHub Actions.

* Keep Claude clear sessions authoritative

Claude /clear arrives as a SessionStart source=clear event, but the hook store only remembered routing data. This change makes the store also own the active session for each workspace, promotes clear starts to Running, and ignores visible Stop/Notification/SessionEnd mutations when their session no longer matches the active workspace session.

Constraint: Claude Code documents SessionStart source=clear for /clear lifecycle boundaries.

Rejected: Only set Running on source=clear | old Stop and SessionEnd events could still clobber the new session afterward.

Confidence: high

Scope-risk: narrow

Directive: Visible Claude hook mutations must pass active-session ownership checks before changing sidebar status or notifications.

Tested: Not run locally per repository policy.

Not-tested: CI red/green proof pending on GitHub Actions.

* Allow Claude sessions to advance turns

A visible hook mutation can only be rejected as stale after the active session boundary is known. Keeping a completed turn id active after Stop made the next prompt in the same Claude session look stale before it could promote its own turn.

The Stop path now refreshes the active session with no turn id once the completed turn has been accepted, preserving stale-session protection across /clear while allowing normal multi-turn prompts to re-enter Running. The regression now exercises that two-turn path before the clear boundary.

Constraint: Do not run local tests; CI owns verification for this branch.
Rejected: Ignore turn id in all current-session checks | would weaken stale turn filtering for late Stop and Notification events.
Confidence: high
Scope-risk: narrow
Directive: Do not persist a completed turn id past Stop without proving the next prompt-submit can promote a new turn.
Tested: git diff --check
Not-tested: Local unit/regression tests per project policy

* Remove duplicate Claude clear helper

The branch integration accidentally kept two isClaudeClearSessionStart definitions in CLI/cmux.swift. The duplicate version referenced a non-existent parsedInput.source property, so Swift would reject the file before CI could exercise the hook lifecycle tests.

Keep the existing implementation that reads source from the parsed hook object and delete only the duplicate helper.

Constraint: Fix review-reported compile blocker without changing lifecycle behavior.
Rejected: Add source to ClaudeHookParsedInput | unnecessary for this compile fix and broader than the failing duplicate.
Confidence: high
Scope-risk: narrow
Directive: Keep exactly one Claude clear source helper unless the parsed input model is intentionally extended.
Tested: git diff --check; rg confirms a single isClaudeClearSessionStart definition and no parsedInput.source reference.
Not-tested: Local tests per project policy

* Restore Claude hook compileability after lifecycle merge

The active-session merge left behind the old source-property helper alongside the newer compact-payload helper. Removing the duplicate keeps source=clear detection on the JSON payload and avoids both the redeclaration and missing-property errors.

Constraint: Do not run local tests; CI owns verification for this branch
Rejected: Reintroduce ClaudeHookParsedInput.source | duplicates state already retained in the compact hook payload
Confidence: high
Scope-risk: narrow
Tested: git diff --check; rg verified a single isClaudeClearSessionStart definition and no parsedInput.source references
Not-tested: Local compile/test execution per task policy

* Normalize Claude active-session cleanup keys

The active-session map is keyed by normalized workspace id, so cleanup after consuming a session must use the same normalized key. Otherwise a record containing incidental whitespace could leave a stale active-session entry behind.

Constraint: Address reviewer-reported lifecycle cleanup edge case without changing visible hook behavior.
Rejected: Store raw workspace ids as active map keys | inconsistent with existing upsert normalization and lookup guards.
Confidence: high
Scope-risk: narrow
Directive: Any activeSessionsByWorkspace lookup should use the normalized workspace key, matching insertion.
Tested: git diff --check
Not-tested: Local tests per project policy

* Harden CI Zig downloads against transient upstream failures

CircleCI and the activation workflow both depend on ziglang.org tarballs during remote macOS setup. A transient 500 from that host failed the debug build before any project code compiled, so the install path now retries downloads and avoids unnecessary Homebrew update/cleanup churn on CircleCI.

Constraint: CI must be made green remotely without running local tests or local xcodebuild.

Rejected: Push an empty commit to rerun CI | would leave the same upstream download flake unchanged.

Confidence: high

Scope-risk: narrow

Directive: Keep Zig installer retries in remote CI setup paths; failures here happen before project build logic runs.

Tested: git diff --check

Not-tested: Local tests and local builds not run per repository/user policy.

* Cover stale Claude session-end lifecycle

Greptile identified that stale SessionEnd exercises a different clear-state path than stale Stop. The existing active-session guard already blocks the mutation, so the regression now drives that hook directly and asserts the active /clear session keeps its status, PID, and notifications intact.

Constraint: Do not run local tests; CI is the verification source for this branch.

Rejected: Add another Swift integration test | the existing Python hook harness already executes the CLI handler through the socket path used by CI.

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repo policy.

Not-tested: Local test execution.

* Stop activation CI from stalling on Zig downloads

The activation workflow was cancelled before build because repeated ziglang.org transfers crawled for the full job timeout. Prefer Homebrew's pinned zig@0.15 bottle on macOS runners, then keep the direct tarball path as a bounded fallback with connection, total-time, and minimum-speed limits.

Constraint: The failed check never reached project build or tests; the only failing surface was CI tool bootstrap.

Rejected: Increase the job timeout | it would hide the bootstrap failure and delay feedback.

Confidence: medium

Scope-risk: narrow

Directive: Keep activation workflow tool bootstrap bounded so performance CI reaches the benchmark or fails quickly.

Tested: git diff --check

Not-tested: Local workflow execution, per repository and user instruction.

* Gate Claude session-end cleanup on the consumed workspace

Greptile noted that session-end computed the current-session guard from the fallback workspace while clearing the consumed session's workspace. Move the guard next to the mutation target so stale cleanup and visible cleanup always evaluate the same workspace identity.

Constraint: This is follow-up review hardening on the existing active-session model.

Rejected: Collapse activeSessionsByWorkspace into sessions in this PR | per-workspace current-session lookup is the intended lifecycle boundary for stale event gating.

Confidence: high

Scope-risk: narrow

Directive: SessionEnd visible cleanup must be gated against the workspace being cleared, not an earlier fallback lookup.

Tested: git diff --check

Not-tested: Local test execution, per repository and user instruction.

* Gate Claude session-end cleanup on consumed workspace

A late SessionEnd can be resolved through fallback surface lookup, so the workspace used to find a record is not always the workspace whose visible state would be cleared. Move the staleness check after consume() and evaluate it against the consumed session's workspace, where the clear_status and notification cleanup actually run.

Constraint: Hook events are delivered by short-lived CLI processes and must tolerate stale or partial Claude payloads

Rejected: Keep the pre-consume fallback workspace guard | it can validate one workspace while clearing another

Confidence: high

Scope-risk: narrow

Tested: Added Swift integration regression for stale SessionEnd fallback cleanup

Not-tested: Local tests not run per repository policy

* Make stale Claude session-end test consume the seeded session

Greptile caught that the Swift regression used an unknown session id, so the hook returned before reaching the intended consumed-session visibility guard. Use the seeded stale session id in the SessionEnd payload so the test exercises consume(), then verifies that stale visible cleanup is blocked.

Constraint: Address high-priority review feedback without running local tests.

Rejected: Leave Python-only coverage | the Swift regression would continue passing for the wrong reason.

Confidence: high

Scope-risk: narrow

Directive: Stale session-end regressions should consume a known stale session before asserting visible cleanup is skipped.

Tested: git diff --check

Not-tested: Local tests per repository and user instruction

* Cover fallback Claude session-end consumption

The stale SessionEnd regression should prove the handler consumed the stale session through fallback lookup before deciding whether visible state may be cleared. Use an unknown late session id and assert the seeded stale session is removed from the store, so the test cannot pass without exercising the consumed-workspace guard.

Constraint: Review feedback flagged the prior Swift regression as able to pass without covering the intended guard

Rejected: Use the stored stale session id directly | that only covers the ordinary mapped-session stale path

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repository policy; CI will run the Swift regression

Not-tested: Local XCTest execution

* Keep Claude clear ownership panel-scoped

The clear SessionStart path now owns the active Claude boundary only for explicit clear events, so late startup/resume SessionStart events from the previous session cannot reclaim the workspace before their stale Stop or SessionEnd arrives. The same clear path now passes the resolved surface id into status updates so split panels receive the Running state in the intended pane.

Constraint: Local test execution is disallowed by repository policy; CI remains the behavioral gate.

Rejected: Let every SessionStart mark active | late non-clear events can overwrite the clear boundary.

Rejected: Drop fail-open isCurrent behavior | transient store errors should not hide legitimate current status updates.

Confidence: medium

Scope-risk: moderate

Directive: Do not let non-clear Claude SessionStart events replace an explicit /clear active boundary without adding event ordering metadata.

Tested: python3 -m py_compile tests/test_claude_hook_clear_running_status.py; git diff --check; conflict marker scan.

Not-tested: Local Swift/unit/UI test execution per repo policy.

* Protect Claude clear state from stale session cleanup

Late hook events can arrive after an explicit /clear boundary. The SessionStart handler now skips PID registration when the event is stale against the active session, and SessionEnd consumption preserves the active session when the incoming turn id is older than the stored active turn.

Constraint: Local test execution is disallowed by repository policy; CI remains the behavioral gate.

Rejected: Clear active ownership before checking currentness | same-session stale SessionEnd would fail open and apply cleanup.

Rejected: Keep prefix-only command assertions | option ordering changes would make the regression test brittle.

Confidence: medium

Scope-risk: moderate

Directive: Do not consume a Claude session or replace its PID from a hook event that is stale relative to active session/turn state.

Tested: python3 -m py_compile tests/test_claude_hook_clear_running_status.py; git diff --check; conflict marker scan.

Not-tested: Local Swift/unit/UI test execution per repo policy.

* Allow new Claude sessions to replace stopped owners

A stopped Claude turn must keep the same session eligible for the next turn, but it must not strand the workspace if that process exits before session-end. Mark stopped ownership as replaceable and let only session activation paths use that escape hatch; clear-session ownership remains non-replaceable so stale pre-clear events stay blocked. Session-end fallback cleanup now checks the consumed session id so missing input session ids do not fail open.\n\nConstraint: /clear SessionStart must continue to suppress stale pre-clear startup, stop, and session-end events\nRejected: Clear active ownership on Stop | breaks same-session multi-turn prompt-submit currentness\nConfidence: medium\nScope-risk: narrow\nTested: git diff --check; conflict-marker scan\nNot-tested: local XCTest per repo policy; CI will run cmux unit regressions

* test: cover codex hooks TOML features section

* fix: harden hook config and auth token cache

* test: cover sign-out browser auth loading cleanup

* fix: cancel browser auth on sign-out

* test: cover codex config read failures

* fix: fail closed on codex config read errors

* test: cover auth sign-out callback race

* fix: discard auth callback after sign-out

* fix: keep claude subcommands out of hook injection

* fix: dismiss stale sparkle update replies

* fix: redact auth logs while preserving observability

* fix: reset update checks and guard sign-out races

* fix: preserve update metadata from driver state

* fix: await auth token assertions before comparing

* fix: address wrapper and auth review feedback

* fix: address session and config review feedback

* ci: retrigger pending checks

* fix: clear stale ime and auth token state

* fix: clean legacy codex hooks config

* test: cover legacy codex hooks markers

* fix: clean empty codex features table

* fix: preserve browser key reentry dispatch

* Fix new-workspace caller window routing (#4042)

* test: cover new workspace caller routing

* fix: route new workspace to caller context

* Add iMessage workspace insertion regression test

* Reset Kitty keyboard mode at shell prompt boundaries (#3870)

* Prove stale Kitty keyboard state leaks CSI-u key bytes

The regression exercises a hosted Ghostty terminal, leaves Kitty keyboard protocol enabled as a crashed TUI would, mirrors the clear-history socket handler clear_screen path, and captures raw PTY stdin bytes for a plain c key. Current behavior should encode CSI-u instead of a single ASCII byte, making the test fail until the protocol state is reset at the shell prompt boundary.

Constraint: Regression must cover PTY input bytes, not source text shape
Confidence: high
Scope-risk: narrow
Directive: Keep this test on the real ghostty_surface_key path; sendText alone bypasses the keyboard encoder
Tested: Manual current-main repro captured c9;1:3uc9;1:3uc9;1:3u after Kitty enable plus clear-history
Not-tested: Local unit execution deferred to CI per requested red/green workflow

* Reset stale Kitty keyboard mode at prompt boundaries

A crashed TUI can leave Ghostty's Kitty keyboard protocol stack pushed after clear-history, causing normal shell input to be encoded as CSI-u. Resetting the stack from the shell prompt hook makes the prompt boundary the ownership point for returning interactive shells to plain byte input.

Constraint: Fix must run through shell integration because Ghostty keeps protocol state inside the terminal surface

Rejected: Reset only in clear-history socket path | stale state also leaks after any crashed TUI returns to prompt

Confidence: high

Scope-risk: narrow

Directive: Keep prompt-boundary reset paired across bash and zsh integrations

Tested: Not run locally per repository testing policy; regression added in prior commit

Not-tested: CI not yet completed

* Clarify Kitty reset fixture failures

The stale keyboard regression now fails at the fixture boundary if the zsh integration does not emit the expected reset bytes, instead of falling through to a misleading PTY byte mismatch.

Constraint: Repository policy forbids local test execution for this PR loop

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repository policy; CI will run the affected XCTest

Not-tested: Local XCTest execution

* chore: retrigger Vercel checks

* Add Kitty reset shell hook coverage

* Reset all terminal keyboard protocols at prompt

* Fix terminal keyboard reset test expectations

* Clarify disabling agent session auto-resume for #3640 (#3991)

* docs: explain disabling agent auto resume

* docs: name auto resume config path

* Fix stale restored agent resume state

* Harden restored agent hook liveness

* refactor: share restored agent normalization

* Honor iMessage workspace ordering and previews

* Add workspace cwd inheritance setting (#3921)

* feat: add workspace cwd inheritance setting

* fix: align workspace cwd setting key naming

* fix: apply workspace cwd setting to detached creation

* fix: expose workspace cwd inheritance setting

* fix: route pane break through detached workspace creation

* fix: keep pane break response pane ids non-null

* fix: distinguish pane break resolution errors

* Approve installed Codex hooks (#4035)

* Approve installed Codex hooks

* Address Codex hook trust review feedback

* Avoid tomllib in Codex hook tests

* Align Codex hook trust test mirror

* Harden Codex hook trust ownership

* Preserve legacy Codex hook cleanup

* Fix workspace unit test after merge

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Revert "Approve installed Codex hooks (#4035)" (#4074)

This reverts commit e4546b7675a4ffa5a41a5429218b60f4e7644e21.

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Fix workspace unit test transfer resume state (#4076)

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Clarify in cmux --help that reload-config covers Ghostty config too (#4060)

* Clarify cmux help that reload-config covers Ghostty config too

`cmux reload-config` reloads BOTH ~/.config/cmux/cmux.json and Ghostty
config (~/.config/ghostty/config) and refreshes terminals in place, but
the help/docs only mentioned cmux.json. Agents (and humans) reading the
help would think they had to restart cmux after editing Ghostty config.

- cmux --help "Agent Help" now tells agents where Ghostty config lives
  and that reload-config picks it up live.
- cmux docs settings, cmux settings path, cmux config --help now list
  ~/.config/ghostty/config as a related (not cmux-owned) location and
  describe reload-config's actual scope.
- cmux schema sidebarAppearance.tintOpacity description now notes it's
  sidebar-only, and points to Ghostty background-opacity / blur for
  terminal transparency.
- skills/cmux/SKILL.md mirrors the wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Align ghostty_config JSON shape across CLI surfaces

Cursor Bugbot and Greptile both flagged that `cmux settings path --json`
emitted `ghostty_config` as a string while `cmux docs settings --json`
emitted it as an object {path, note}. An agent reading both outputs with
the same key expectation would have to special-case the type.

Standardize on the object shape with `path` and `note` in both, matching
docsPayload. This is the agent-discoverability path the PR is trying to
make reliable, so making the shape consistent is on-purpose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use canonical Ghostty config key background-blur (not background-blur-radius)

CodeRabbit flagged that `background-blur-radius` is outdated. Verified
against the Ghostty submodule at ghostty/src/config/Config.zig: line 70
declares `background-blur-radius` as a compatibilityRenamed alias for
`background-blur`. The current canonical key is `background-blur` and it
accepts the same integer value (e.g. `background-blur = 20`).

Update the two docs that introduced the alias name:
- skills/cmux/SKILL.md
- web/data/cmux.schema.json (sidebarAppearance.tintOpacity description)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add nucleo FFI command palette benchmark

* Open right sidebar tools as panes (#4065)

* Open right sidebar tools as panes

* Remove unreachable sidebar pane commands

* Fix sidebar pane unit test build

* Address sidebar pane review feedback

* Fix vault pane focus tracking

* Address right sidebar pane review followups

* Use modern sidebar pane flash observer

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Save crash diagnostics under cmux state (#4077)

* Save crash diagnostics under cmux state

* fix: key GhosttyKit artifacts by crash path

* fix: pin cmux crash GhosttyKit archive

* fix: mark crash breadcrumb scan concurrent

* fix: keep crash scan compatible with Xcode 16

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Menubar global search P1 (#3908)

* Ship local global search as a remappable menubar flow

Phase 1 needs browser and markdown value without terminal scrollback, so the index owns durable FTS5 upserts while AppDelegate keeps one navigation path from palette rows to focused panels. The global shortcut is wired through the existing shortcut settings instead of a standalone Carbon shim so Settings and cmux.json remain authoritative.

Constraint: Phase 1 excludes Ghostty terminal scrollback capture

Constraint: User required no local test execution and no reload before CI is green

Rejected: Hardcoded GlobalSearchHotkey shim | violates KeyboardShortcutSettings policy

Confidence: medium

Scope-risk: broad

Directive: Keep future terminal capture feeding SearchIndex documents through GlobalSearchCoordinator rather than adding another palette/navigation path

Tested: git diff --check; python3 -m json.tool Resources/Localizable.xcstrings; python3 -m json.tool web/data/cmux.schema.json; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Not-tested: Local unit/UI tests and tagged app launch per task constraints

* Remove duplicate search refresh work

Cursor review pointed out that the menubar toggle and palette onAppear both refreshed the live index. Keeping the refresh in the palette lifecycle avoids resetting browser debounce tasks while still indexing each time the palette opens, and removing the unused workspace delete API keeps the storage surface honest.

Constraint: Review feedback came from PR #3908 after the first CI pass started

Rejected: Keep both refresh calls | causes avoidable debounce cancellation and slower browser result availability

Confidence: high

Scope-risk: narrow

Directive: Add workspace-level deletion only when a real workspace teardown caller is wired

Tested: git diff --check; python3 -m json.tool Resources/Localizable.xcstrings; python3 -m json.tool web/data/cmux.schema.json; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Not-tested: Local tests/build per task constraints

* Fix global search stale index entries

* Address global search review feedback

* Fix search query token mapping compile error

* Fix global search CI failures

* Fix stale unavailable markdown search entries

* Cancel stale markdown search captures

* Cancel global search refresh on dismiss

* Fix global search review followups

* Address global search post-CI feedback

* Preserve markdown panel title search on read failure

* Address global search lifecycle feedback

* Address global search review lifecycle feedback

* Refine global search capture ownership

* fix: address global search review blockers

* feat: show open panels in global search

* fix: cover right sidebar tool panel in search

* Fix cmuxTests: rename restorableAgentAutoResumePending to restorableAgentResumeState (#4068)

* fix: handle repeated assistant imessage completions

* Open supported files in cmux on cmd-click (#4041)

* Open supported files in cmux on cmd-click

* Add cmd-click file preview verification script

* Reuse right pane for cmd-click file previews

* Keep cmd-click UI test terminal after preview focus

* Accept numeric cmd-click test payload values

* Capture cmd-click UI test window snapshots

* Add file-type-aware external open actions

* Address supported file routing review feedback

* Fix external open menu sendability warnings

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Suppress native proxy icon on cmux main windows (#3973)

* Add proxy icon regression test

* Suppress native proxy icon on cmux windows

* chore: retrigger preview deployments

* Refine native proxy icon suppression

* fix: keep titlebar folder icon aligned

* fix: restore titlebar folder icon leading offset

* test: cover folder icon frame replacement

* fix: resync folder icon on frame replacement

* test: cover folder icon ancestor movement

* fix: track folder icon ancestor movement

* fix: address folder icon review feedback

* test: stabilize folder icon ancestor sync

* fix: handle sidebar tool panels in global search

---------

Co-authored-by: austinpower1258 <austinwang115@gmail.com>

* Close browser panels when pages request window close (#4070)

* Add browser self-close restore regression test

* Close browser panels from WebKit close callbacks

* fix: index right sidebar tool panels as titles

* fix: keep web close callback synchronous

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add notification policy hooks

Merged https://github.com/manaflow-ai/cmux/pull/4054

* Fix sidebar unread badge after re-marking notifications (#4084)

* Add sidebar unread notification regression test

* Keep sidebar notification badge live during menu freeze

* Cover sidebar presentation fallback cases

* Limit Cloud VMs by active provider state (#4046)

* test: cover active vm limit with paused freestyle vms

* fix: enforce cloud vm limits by active state

* fix: parallelize cloud vm status refresh

* chore: update web security dependencies

* fix: handle right sidebar tools in global search

* fix: guard cloud vm status refresh races

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Fix Settings search synonyms (#4082)

* Add settings search synonym regressions

* Fix settings search synonyms

* Add shortcut bindings anchor regression

* Cover clickable PR settings search alias

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add tagged debug CLI helper (#4092)

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add unread defer shortcut (#4086)

* Add unread defer shortcut

* fix: address unread defer feedback

* fix: keep manual unread jump explicit

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Document notification hooks default off

Document that notification hooks are off by default and style the config key in localized docs.

* Approve installed Codex hooks after dogfood (#4075)

* Reapply Codex hook approval changes for dogfood

* Notify on Codex plan input requests

* Handle Codex plan question transcript items

* Address Codex hook review feedback

* Recover malformed Codex hook trust blocks

* Avoid duplicate consecutive cmux hook reinserts

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Document session restore behavior

Adds session restore docs, blog, README updates, and review cleanup.

* Use nucleo for command palette search

* Skip unrestorable Claude startup sessions

PR: https://github.com/manaflow-ai/cmux/pull/4079

* Revert "Suppress native proxy icon on cmux main windows" (#4099)

* Revert "Suppress native proxy icon on cmux main windows (#3973)"

This reverts commit 5048440ff44b4edbe328b65403724ff79476b2e6.

* Fix titlebar proxy icon without detached panel sync

* Apply proxy icon override to fallback config

* Restore key regain redraw invariant

* Tune nucleo palette initialism ranking

* Fix terminal portal resize lag (#4102)

* Fix Korean 2-Set terminal arrows (#4095)

* Add Korean 2-Set arrow IME regression

* Restore Korean 2-Set arrow forwarding

* Restore Zhuyin IME command routing

* Address IME review feedback

* Fix Korean IME regression test compile

* Address IME review follow-ups

* Address Bopomofo preedit review feedback

* Avoid idle Zhuyin key suppression

* Remove dead text input wrapper

* Address IME suppression review feedback

* Fix palette stitched highlight precedence

* Add Codex Teams subagent panes

* Open markdown files in preview panels from cmux open (#4085)

* fix: open markdown files in preview panels

* fix: preserve markdown viewer transparency

* fix: mute markdown open-with header button

* fix: align markdown header controls

* fix: align file header controls

* fix: address markdown review feedback

* test: cover opaque text editor alpha

* fix: align header open fallback

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add task manager sorting and program aggregates (#4066)

* Add task manager sorting and program aggregates

* Add sorting to cmux top output

* Add flat TSV cmux top output

* Add coding agent task manager totals

* Make task manager program totals payload-backed

* Recognize agent launcher process names in task manager

* Fix task manager test build helpers

* Recognize Claude versioned launcher processes

* Show loading state before task manager sample

* Recognize versioned agent process names

* Use agent totals for task manager hierarchy icons

* fix: address task manager review feedback

* fix: address follow-up task manager review

* fix: address final task manager review

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Keep manual unread sticky until terminal interaction (#4104)

* test: cover sticky manual unread state

* fix: keep manual unread sticky until terminal input

* fix: show workspace manual unread pane ring

* fix: sync manual unread badge on focus changes

* fix: stabilize manual unread representative fallback

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: austinpower1258 <austinwang115@gmail.com>

* Bump version to 0.64.5 (#4107)

* Fix Pi Vault icon and JSONL titles (#4120)

* test: cover Pi JSONL content block titles

* fix: parse Pi JSONL text blocks for Vault titles

* fix: align Pi JSONL title role handling

* fix: require typed text blocks for Pi titles

* Improve Cloud VM error guidance (#4094)

* Improve Cloud VM error guidance

* Address final Cloud VM review feedback

* Narrow Cloud VM sanitizer env var block

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Keep selected workspace visible after sidebar reorders

* Add sidebar scroll regression for workspace move to top

* Reveal selected workspace after sidebar reorders

* Handle right sidebar tool panels in global search

* Test workspace move to top visibility only

* Refine sidebar reorder scroll trigger

* Add move-to-top notification regression

* Skip no-op move-to-top notifications

* Assert no-op move-to-top keeps order

* Require matching manager for reorder scroll

* Scroll selected workspace on index shifts

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Use transparent backgrounds for file preview panels (#4088)

* Handle right sidebar tools in global search browse hits

* Use transparent backgrounds for file preview panels

* Cover panel theme background preservation

* Fix PDF file preview open menu

* Make file open menu button compact

* Fix PDF open menu chrome button

* Fix PDF open-with chrome click target

* Address file preview chrome review feedback

* Fix PDF background cache invalidation

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Inherit stdin in backgrounded ssh inside startup wrapper (#4135)

* Add regression test for SSH startup wrapper dropping stdin

After PR #3786 backgrounded the ssh invocation inside the startup
wrapper for signal/reconnect handling, `cmux ssh <host>` sessions
stopped forwarding keystrokes from the surface PTY to the remote
shell. Output flowed back (the remote prompt rendered in cmux UI),
but anything typed never reached zsh on the other side, which sat
blocked in `do_poll` on the remote pts.

Root cause: POSIX sh redirects stdin of an async command (`&`) to
/dev/null when job control is off — the default for the `/bin/sh -c …`
that runs the startup wrapper. Without an explicit `<&0` on the
`&`'d ssh line, the local PTY stdin is silently dropped.

This commit adds the failing regression test. The fix follows in
the next commit so CI can prove the test catches the bug.

* Inherit stdin in backgrounded ssh inside startup wrapper

PR #3786 wrapped the ssh invocation in `while :; do … & wait` to enable
SIGHUP/INT/TERM trap handling and reconnect-on-exit-255. Backgrounding
ssh, however, drops its stdin: POSIX sh redirects fd 0 of any async
command to /dev/null when job control is off, which is the default for
the `/bin/sh -c …` host that runs this wrapper.

As a result, output from the remote still flowed back to the surface
PTY (ssh's stdout/stderr stayed wired) but the user's keystrokes never
reached the remote — they hit ttysNNN on the Mac side, kernel echoed
them locally, but ssh's stdin was /dev/null so nothing was forwarded.
zsh on the far side sat in `do_poll` forever.

Explicit `<&0` on both the `command` line and the `( … )` shell-snippet
form overrides the POSIX default and re-attaches the wrapper's own
stdin to the backgrounded ssh process.

Verified with the regression test added in the previous commit, plus
manual repro on Darwin:

    /bin/sh -c 'cat &  wait'           # cat's fd 0 → /dev/null (bug)
    /bin/sh -c 'cat <&0 &  wait'       # cat's fd 0 → parent stdin (fix)

* Add docs search

Adds localized Pagefind docs search with heading anchors and section-aware results.

* Fix Swift interpolation escape in SSH stdin regression test (#4154)

`testSSHStartupForwardsStdinToBackgroundedSSH` used `\"<empty>\"` inside a
`\(...)` string interpolation. Swift parses `\"` as the end of the outer
literal, breaking compilation of cmuxTests on main:

    Cannot find ')' to match opening '(' in string interpolation
    Unterminated string literal

That kept `ci/circleci: macos-unit-tests` red on main after #4135 even
though `macos-debug-build` and `macos-release-build` still passed (the
test target was the only thing affected). The runtime fix (`<&0` on the
backgrounded ssh) is unchanged and was verified end-to-end on a cloud Mac.

Inside an interpolation, the string is already a Swift expression and
literal quotes are unescaped; drop the four backslashes.

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add command palette settings toggles

Adds command palette toggles for boolean Settings rows, including iMessage Mode.

* Ensure Rust toolchain is installed for nucleo FFI builds

* Document nucleo FFI thread and ABI assumptions

* Reuse nucleo index in preview search test helper

* Install Rust for activation perf builds

---------

Co-authored-by: Tobi Lutke <tobi@shopify.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kevin Peng <48529172+kays0x@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request May 15, 2026
* Optimize command palette search

* Reduce command palette typing frame work

* Fix command palette result rendering

* Ignore stale command palette row snapshots

* Use command ids for palette row identity

* Match Zed-style palette result limiting

* Reduce palette preview search frame misses

* Use nucleo for command palette search (#4078)

* feat: improve markdown viewer

* fix: address markdown review feedback

* fix: clean up markdown review issues

* fix: address markdown review feedback

* fix: address latest markdown review

* Fix #3807: bring notification CLI to panel parity (#3811)

* Prove notification CLI parity is missing

Add behavior-level coverage for the missing notification socket and CLI actions before implementing them. The tests drive V2 notification action methods, CLI subcommands, extended list fields, and the UI open-notification flow so CI can show the pre-fix gap.

Constraint: Regression tests must be committed before the implementation for issue #3807

Constraint: Local Swift tests are not run in this repo; verification is through CI

Confidence: high

Scope-risk: narrow

Tested: git diff --check

Not-tested: Swift/XCUITest execution; intentionally deferred to CI

* Make notification actions scriptable from the CLI

The notifications panel already owned the behavior for dismissing, marking read, opening, and jumping to unread rows. This wires the socket and CLI to those existing store/AppDelegate paths, extends list output with panel metadata, and documents the new command surface without introducing a second notification action model.

Constraint: Existing Notifications page behavior must remain the source of truth

Constraint: Direct xcodebuild and local Swift/XCUITest runs are forbidden in this workspace

Rejected: Duplicate CLI-side focus or read-state logic | would diverge from AppDelegate.openNotification and TerminalNotificationStore semantics

Confidence: medium

Scope-risk: moderate

Tested: git diff --check

Tested: jq empty Resources/Localizable.xcstrings

Tested: python3 -m json.tool Resources/Localizable.xcstrings

Not-tested: Swift unit/UI execution; deferred to CI per repo policy

* Make notification CI compile under strict concurrency

The notification parity implementation introduced a shared formatter on a main-actor type and a test helper that crossed actor boundaries through escaping closures. This keeps the socket behavior unchanged while making date formatting local to the main-actor call path and keeping the XCTest socket request helper from capturing actor-isolated state in the background request closure.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace

Rejected: Run xcodebuild locally to confirm | user explicitly warned direct xcodebuild can deadlock the machine

Confidence: medium

Scope-risk: narrow

Tested: git diff --check

Tested: jq empty Resources/Localizable.xcstrings

Not-tested: Swift compilation and XCTest execution; deferred to CI

* Make notification parity actions atomic and exact

Review feedback exposed two behavior risks in the notification parity path: bulk dismissal was implemented as client-side list-and-loop work, and jump-to-unread could report one unread notification while opening a later valid one. The server now owns already-read dismissal as a single V2 action, the jump helper returns the notification it actually opened, and event/list parsing details match those server semantics.

Constraint: Notification action logic must reuse the existing store and AppDelegate paths

Rejected: Split TerminalController into new controllers in this PR | broad refactor is unrelated to issue #3807 and would obscure the parity fix

Confidence: medium

Scope-risk: moderate

Tested: git diff --check

Tested: jq empty Resources/Localizable.xcstrings

Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace instructions

* Expose app test symbols to CLI notification coverage

The integration regression exercises real app-backed notification state through the CLI harness, so the test target must import the built app module the same way the socket action tests do.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace
Rejected: Verify with xcodebuild locally | user explicitly forbids direct xcodebuild
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI

* Make notification list and dismiss payloads stable

Review caught two small contract hazards in the new notification RPC surface: the dismiss result used mixed JSON types, and a pipe in the appended tab title could shift the legacy list parser. The server now reports dismissals as counts consistently and escapes only the new trailing list field, with the CLI decoding it after structural parsing.

Constraint: The V1 list response remains pipe-delimited for backward compatibility, so only newly appended trailing fields can be encoded without changing old parser behavior
Rejected: Replace list_notifications with JSON-only output | existing V1 parsers depend on the line format
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace policy

* Return async notification socket responses from tests

CircleCI caught a compile-only issue in the async V2 test helper: the continuation result was awaited but not returned from the method that promises a response dictionary. Returning the continuation value restores the helper contract without changing the exercised socket behavior.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace
Rejected: Verify with local xcodebuild | user explicitly forbids direct xcodebuild
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI

* Avoid blocking notification CLI integration sockets

The integration regression has to create AppKit-backed notification state on the main actor, but running the CLI subprocess synchronously there can block the socket handler's main-actor store mutations. The test now awaits subprocess work on a background queue and verifies read state through the same CLI list path.

Constraint: Socket notification handlers intentionally hop to the main actor for store and AppDelegate work
Rejected: Run the CLI synchronously from the main actor | it can deadlock the in-process socket server during tests
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace policy

* Harden notification CLI response contracts

The notification parity path now distinguishes new list-notification trailer fields from old pipe-heavy bodies, rejects surface-only mark-read selectors, and returns post-open read state by marking through the shared store path after a successful focus action.

Constraint: list_notifications remains a legacy pipe-delimited protocol, so the new trailer needs its own discriminator while older body parsing keeps joining payload[6...]
Rejected: Add a third list_notifications sentinel field | the issue asked for exactly the two appended fields
Rejected: Rely on AppDelegate delayed mark-read for socket JSON | CLI callers need the open response and subsequent list output to reflect the explicit action
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI per workspace policy

* Make CLI integration helper capture explicit self

CircleCI's Swift compile step requires explicit self when the background subprocess closure calls the test helper method. This keeps the deadlock fix intact while satisfying Swift capture rules.

Constraint: Local Swift tests and direct xcodebuild are explicitly forbidden in this workspace
Rejected: Verify with local xcodebuild | user explicitly forbids direct xcodebuild
Confidence: high
Scope-risk: narrow
Tested: git diff --check
Tested: jq empty Resources/Localizable.xcstrings
Not-tested: Swift compilation and XCTest execution; deferred to CI

* Make CLI presentation flags order-independent

Users naturally put presentation flags next to the command they are inspecting. Normalize --json and --id-format after command selection so docs examples such as cmux list-notifications --json produce JSON while still preserving literal flag-looking values for command options.

Constraint: Do not run reload.sh before CI is green; never run bare xcodebuild.

Rejected: Documentation-only correction | leaves copy-pasted command behavior surprising.

Rejected: Notification-only --json handling | repeats the same parser split for future JSON-capable commands.

Confidence: medium

Scope-risk: moderate

Directive: Keep presentation flag parsing centralized; update commandOptionsWithValues when adding value-taking command options.

Tested: git diff --check

Not-tested: Local XCTest/build per repo policy; CI will verify.

* Fix notification surface selector error message

* Make notification open mark read synchronously

* Fix CLI presentation flag parsing

* Test notification mark-read missing id

* Reject missing notification mark-read ids

* fix: polish markdown viewer dogfood

* Fix shared WebView task manager attribution

Fixes shared WebContent resource attribution in task manager rows and adds regression coverage.

* Prevent display-link crash from terminal portal layout reentry (#3885)

* Pin portal sync deferral during SwiftUI host callbacks

The silent-exit crash path points at SwiftUI/AppKit layout recursion reaching a CATransaction display-link flush. This test locks the intended invariant: geometry callbacks originating from the SwiftUI NSViewRepresentable host must defer portal reconciliation instead of forcing immediate AppKit layout, even while an interactive resize is active.

Constraint: Local tests are intentionally not run in this repository; CI owns regression proof.\nRejected: Assert on source text or unified-log contents | timing-dependent and not executable through the policy seam.\nConfidence: medium\nScope-risk: narrow\nDirective: Keep immediate portal flushing owned by external AppKit resize observers, not SwiftUI host callbacks.\nTested: Not run locally per repository policy and user instruction.\nNot-tested: Full multi-session crash reproduction is timing-dependent and not deterministic.

* Prevent portal layout reentry from terminal host callbacks

The terminal NSViewRepresentable host was allowed to bind into the window portal and synchronously flush AppKit layout from update/layout callbacks. That made SwiftUI's own host layout and the external terminal portal both believe they could drive geometry in the same render turn, matching the NSHostingView reentrant-layout warnings reported before the CATransaction display-link abort.\n\nThe portal now treats SwiftUI host callbacks as state capture only: they register the binding and schedule the portal owner to reconcile geometry after the current render turn. Immediate geometry flushing remains available to the portal's external AppKit resize observers, which are outside SwiftUI body/layout evaluation. The launch path also records clean exits and posts a one-time TerminalNotificationStore breadcrumb when a newer ghostty Breakpad envelope is found after an unclean exit.

Constraint: Local tests and app builds are not run before CI for this branch; verification is delegated to CI, then the required tagged reload.\nRejected: Wrap the CATransaction/display-link exception in @try/@catch | it would hide AppKit's abort symptom without removing the reentrant layout owner split.\nRejected: Keep immediate sync during interactive SwiftUI host callbacks | still allows layoutSubtreeIfNeeded inside the representable layout/update path.\nConfidence: medium\nScope-risk: moderate\nDirective: SwiftUI/AppKit host callbacks must not force terminal portal layout synchronously; add external observer paths for any future immediate resize flushing.\nTested: git diff --check; jq empty Resources/Localizable.xcstrings\nNot-tested: Local XCTest/build/repro per repository and user instructions; 75-minute multi-session crash reproduction is not deterministic.

* Record clean-exit breadcrumb after teardown

The crash breadcrumb should compare Breakpad envelopes against the last successfully completed termination path. Recording the timestamp after teardown avoids marking an exit clean before session persistence, process cleanup, and notification cleanup have finished.

Constraint: This is a follow-up correctness tweak before CI settled.\nRejected: Keep the timestamp at the start of applicationWillTerminate | a crash during termination cleanup could suppress the next-launch breadcrumb.\nConfidence: high\nScope-risk: narrow\nDirective: Only update the clean-exit timestamp after teardown work that must complete for a clean quit.\nTested: git diff --check; jq empty Resources/Localizable.xcstrings\nNot-tested: Local tests/builds per repository and user instructions.

* Defer first portal install from SwiftUI callbacks

A deferred SwiftUI host bind could still create the WindowTerminalPortal for the first time, and the initializer previously installed the host with an immediate layout flush. Threading the same deferral flag through portal creation keeps the invariant complete: representable update/layout callbacks never synchronously flush terminal portal layout, even on first bind.

Constraint: Must preserve immediate install behavior for non-SwiftUI external portal callers
Rejected: Assume portals already exist before host callbacks | first terminal bind in a new window can create one
Confidence: high
Scope-risk: narrow
Directive: Any future portal creation path from SwiftUI callbacks must carry deferred synchronization through initialization
Tested: git diff --check; jq empty Resources/Localizable.xcstrings
Not-tested: Local tests/builds per repository and user instructions

* Move crash breadcrumb work out of launch hot path

The crash breadcrumb scanner is pure Foundation logic, so it now lives in its own source file and performs the crash-directory scan from a detached utility task. App launch only schedules the check once, then posts the existing localized notification after the background scan returns to the main actor. The terminal host geometry policy is also simplified to make the always-deferred invariant explicit at the call site.

Constraint: Local build/tests are intentionally skipped until CI completes per issue instructions
Rejected: Keep synchronous directory enumeration in AppDelegate.configure | startup should not block on crash artifact I/O
Rejected: Preserve the dead immediate portal sync branch | host callbacks are never allowed to force layout synchronously
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce synchronous crash-directory scans or immediate portal layout flushes from SwiftUI host callbacks
Tested: git diff --check; jq empty Resources/Localizable.xcstrings; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj
Not-tested: Local build/tests per repository and user instructions

* Tighten portal geometry regression seam

* Fix crash breadcrumb actor isolation

* Mark crash breadcrumb async helper nonisolated

* Make crash breadcrumb scan concurrent

* fix: remove inert crash breadcrumb cooldown key

* fix: use renamed crash breadcrumb annotation

* fix: own crash breadcrumb scan task

* Hide sidebar descriptions in title-only mode (#4040)

* Hide sidebar descriptions in title-only mode

* Add sidebar description visibility toggle

* Let sidebar titles use trailing slack

* Float sidebar shortcut hints over rows

* Remove unused sidebar width helper

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add Pi agent icon

PR: https://github.com/manaflow-ai/cmux/pull/4057

* Keep Claude Running after clear (#3631)

* Prove Claude clear hook loses running status

The regression exercises the Claude hook handler directly with a SessionStart payload whose source is clear, then sends a late Stop for the previous session. The current handler does not promote the clear boundary to Running or reject stale Stop mutations, so this test is expected to fail before the lifecycle fix.

Constraint: Tests must exercise the hook handler directly rather than driving the full app.

Confidence: high

Scope-risk: narrow

Directive: Keep this regression on the direct CLI hook path; do not replace it with source-text assertions.

Tested: Not run locally per repository policy.

Not-tested: CI red/green proof pending on GitHub Actions.

* Prove Claude clear should own running status

Add hook-level regression coverage for Claude Code /clear. The tests drive the bundled CLI against a mock cmux socket so CI proves SessionStart(source=clear) must set the visible Running status and a prior session Stop must not clobber that fresh lifecycle.

Constraint: Local tests are intentionally not run; CI owns test execution for this task.
Confidence: high
Scope-risk: narrow
Tested: Not run locally per instruction
Not-tested: Full app UI repro under CI

* Ignore stale Claude hook events after clear

Claude /clear starts a fresh hook lifecycle but the sidebar status key is shared at the workspace level. Persist the active Claude session per workspace, promote clear SessionStart to a visible Running state, and ignore teardown or notification mutations from sessions that are no longer current.

Constraint: Claude Code emits /clear as SessionStart(source=clear).
Rejected: Only set Running on clear SessionStart | late Stop from the previous lifecycle could still clobber the new status.
Confidence: high
Scope-risk: narrow
Directive: Future Claude hook status mutations must respect the active workspace session before touching claude_code.
Tested: Not run locally per instruction
Not-tested: Full app UI after CI

* Prove Claude clear hook loses running status

The regression exercises the Claude hook handler directly with a SessionStart payload whose source is clear, then sends a late Stop for the previous session. The current handler does not promote the clear boundary to Running or reject stale Stop mutations, so this test is expected to fail before the lifecycle fix.

Constraint: Tests must exercise the hook handler directly rather than driving the full app.

Confidence: high

Scope-risk: narrow

Directive: Keep this regression on the direct CLI hook path; do not replace it with source-text assertions.

Tested: Not run locally per repository policy.

Not-tested: CI red/green proof pending on GitHub Actions.

* Keep Claude clear sessions authoritative

Claude /clear arrives as a SessionStart source=clear event, but the hook store only remembered routing data. This change makes the store also own the active session for each workspace, promotes clear starts to Running, and ignores visible Stop/Notification/SessionEnd mutations when their session no longer matches the active workspace session.

Constraint: Claude Code documents SessionStart source=clear for /clear lifecycle boundaries.

Rejected: Only set Running on source=clear | old Stop and SessionEnd events could still clobber the new session afterward.

Confidence: high

Scope-risk: narrow

Directive: Visible Claude hook mutations must pass active-session ownership checks before changing sidebar status or notifications.

Tested: Not run locally per repository policy.

Not-tested: CI red/green proof pending on GitHub Actions.

* Allow Claude sessions to advance turns

A visible hook mutation can only be rejected as stale after the active session boundary is known. Keeping a completed turn id active after Stop made the next prompt in the same Claude session look stale before it could promote its own turn.

The Stop path now refreshes the active session with no turn id once the completed turn has been accepted, preserving stale-session protection across /clear while allowing normal multi-turn prompts to re-enter Running. The regression now exercises that two-turn path before the clear boundary.

Constraint: Do not run local tests; CI owns verification for this branch.
Rejected: Ignore turn id in all current-session checks | would weaken stale turn filtering for late Stop and Notification events.
Confidence: high
Scope-risk: narrow
Directive: Do not persist a completed turn id past Stop without proving the next prompt-submit can promote a new turn.
Tested: git diff --check
Not-tested: Local unit/regression tests per project policy

* Remove duplicate Claude clear helper

The branch integration accidentally kept two isClaudeClearSessionStart definitions in CLI/cmux.swift. The duplicate version referenced a non-existent parsedInput.source property, so Swift would reject the file before CI could exercise the hook lifecycle tests.

Keep the existing implementation that reads source from the parsed hook object and delete only the duplicate helper.

Constraint: Fix review-reported compile blocker without changing lifecycle behavior.
Rejected: Add source to ClaudeHookParsedInput | unnecessary for this compile fix and broader than the failing duplicate.
Confidence: high
Scope-risk: narrow
Directive: Keep exactly one Claude clear source helper unless the parsed input model is intentionally extended.
Tested: git diff --check; rg confirms a single isClaudeClearSessionStart definition and no parsedInput.source reference.
Not-tested: Local tests per project policy

* Restore Claude hook compileability after lifecycle merge

The active-session merge left behind the old source-property helper alongside the newer compact-payload helper. Removing the duplicate keeps source=clear detection on the JSON payload and avoids both the redeclaration and missing-property errors.

Constraint: Do not run local tests; CI owns verification for this branch
Rejected: Reintroduce ClaudeHookParsedInput.source | duplicates state already retained in the compact hook payload
Confidence: high
Scope-risk: narrow
Tested: git diff --check; rg verified a single isClaudeClearSessionStart definition and no parsedInput.source references
Not-tested: Local compile/test execution per task policy

* Normalize Claude active-session cleanup keys

The active-session map is keyed by normalized workspace id, so cleanup after consuming a session must use the same normalized key. Otherwise a record containing incidental whitespace could leave a stale active-session entry behind.

Constraint: Address reviewer-reported lifecycle cleanup edge case without changing visible hook behavior.
Rejected: Store raw workspace ids as active map keys | inconsistent with existing upsert normalization and lookup guards.
Confidence: high
Scope-risk: narrow
Directive: Any activeSessionsByWorkspace lookup should use the normalized workspace key, matching insertion.
Tested: git diff --check
Not-tested: Local tests per project policy

* Harden CI Zig downloads against transient upstream failures

CircleCI and the activation workflow both depend on ziglang.org tarballs during remote macOS setup. A transient 500 from that host failed the debug build before any project code compiled, so the install path now retries downloads and avoids unnecessary Homebrew update/cleanup churn on CircleCI.

Constraint: CI must be made green remotely without running local tests or local xcodebuild.

Rejected: Push an empty commit to rerun CI | would leave the same upstream download flake unchanged.

Confidence: high

Scope-risk: narrow

Directive: Keep Zig installer retries in remote CI setup paths; failures here happen before project build logic runs.

Tested: git diff --check

Not-tested: Local tests and local builds not run per repository/user policy.

* Cover stale Claude session-end lifecycle

Greptile identified that stale SessionEnd exercises a different clear-state path than stale Stop. The existing active-session guard already blocks the mutation, so the regression now drives that hook directly and asserts the active /clear session keeps its status, PID, and notifications intact.

Constraint: Do not run local tests; CI is the verification source for this branch.

Rejected: Add another Swift integration test | the existing Python hook harness already executes the CLI handler through the socket path used by CI.

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repo policy.

Not-tested: Local test execution.

* Stop activation CI from stalling on Zig downloads

The activation workflow was cancelled before build because repeated ziglang.org transfers crawled for the full job timeout. Prefer Homebrew's pinned zig@0.15 bottle on macOS runners, then keep the direct tarball path as a bounded fallback with connection, total-time, and minimum-speed limits.

Constraint: The failed check never reached project build or tests; the only failing surface was CI tool bootstrap.

Rejected: Increase the job timeout | it would hide the bootstrap failure and delay feedback.

Confidence: medium

Scope-risk: narrow

Directive: Keep activation workflow tool bootstrap bounded so performance CI reaches the benchmark or fails quickly.

Tested: git diff --check

Not-tested: Local workflow execution, per repository and user instruction.

* Gate Claude session-end cleanup on the consumed workspace

Greptile noted that session-end computed the current-session guard from the fallback workspace while clearing the consumed session's workspace. Move the guard next to the mutation target so stale cleanup and visible cleanup always evaluate the same workspace identity.

Constraint: This is follow-up review hardening on the existing active-session model.

Rejected: Collapse activeSessionsByWorkspace into sessions in this PR | per-workspace current-session lookup is the intended lifecycle boundary for stale event gating.

Confidence: high

Scope-risk: narrow

Directive: SessionEnd visible cleanup must be gated against the workspace being cleared, not an earlier fallback lookup.

Tested: git diff --check

Not-tested: Local test execution, per repository and user instruction.

* Gate Claude session-end cleanup on consumed workspace

A late SessionEnd can be resolved through fallback surface lookup, so the workspace used to find a record is not always the workspace whose visible state would be cleared. Move the staleness check after consume() and evaluate it against the consumed session's workspace, where the clear_status and notification cleanup actually run.

Constraint: Hook events are delivered by short-lived CLI processes and must tolerate stale or partial Claude payloads

Rejected: Keep the pre-consume fallback workspace guard | it can validate one workspace while clearing another

Confidence: high

Scope-risk: narrow

Tested: Added Swift integration regression for stale SessionEnd fallback cleanup

Not-tested: Local tests not run per repository policy

* Make stale Claude session-end test consume the seeded session

Greptile caught that the Swift regression used an unknown session id, so the hook returned before reaching the intended consumed-session visibility guard. Use the seeded stale session id in the SessionEnd payload so the test exercises consume(), then verifies that stale visible cleanup is blocked.

Constraint: Address high-priority review feedback without running local tests.

Rejected: Leave Python-only coverage | the Swift regression would continue passing for the wrong reason.

Confidence: high

Scope-risk: narrow

Directive: Stale session-end regressions should consume a known stale session before asserting visible cleanup is skipped.

Tested: git diff --check

Not-tested: Local tests per repository and user instruction

* Cover fallback Claude session-end consumption

The stale SessionEnd regression should prove the handler consumed the stale session through fallback lookup before deciding whether visible state may be cleared. Use an unknown late session id and assert the seeded stale session is removed from the store, so the test cannot pass without exercising the consumed-workspace guard.

Constraint: Review feedback flagged the prior Swift regression as able to pass without covering the intended guard

Rejected: Use the stored stale session id directly | that only covers the ordinary mapped-session stale path

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repository policy; CI will run the Swift regression

Not-tested: Local XCTest execution

* Keep Claude clear ownership panel-scoped

The clear SessionStart path now owns the active Claude boundary only for explicit clear events, so late startup/resume SessionStart events from the previous session cannot reclaim the workspace before their stale Stop or SessionEnd arrives. The same clear path now passes the resolved surface id into status updates so split panels receive the Running state in the intended pane.

Constraint: Local test execution is disallowed by repository policy; CI remains the behavioral gate.

Rejected: Let every SessionStart mark active | late non-clear events can overwrite the clear boundary.

Rejected: Drop fail-open isCurrent behavior | transient store errors should not hide legitimate current status updates.

Confidence: medium

Scope-risk: moderate

Directive: Do not let non-clear Claude SessionStart events replace an explicit /clear active boundary without adding event ordering metadata.

Tested: python3 -m py_compile tests/test_claude_hook_clear_running_status.py; git diff --check; conflict marker scan.

Not-tested: Local Swift/unit/UI test execution per repo policy.

* Protect Claude clear state from stale session cleanup

Late hook events can arrive after an explicit /clear boundary. The SessionStart handler now skips PID registration when the event is stale against the active session, and SessionEnd consumption preserves the active session when the incoming turn id is older than the stored active turn.

Constraint: Local test execution is disallowed by repository policy; CI remains the behavioral gate.

Rejected: Clear active ownership before checking currentness | same-session stale SessionEnd would fail open and apply cleanup.

Rejected: Keep prefix-only command assertions | option ordering changes would make the regression test brittle.

Confidence: medium

Scope-risk: moderate

Directive: Do not consume a Claude session or replace its PID from a hook event that is stale relative to active session/turn state.

Tested: python3 -m py_compile tests/test_claude_hook_clear_running_status.py; git diff --check; conflict marker scan.

Not-tested: Local Swift/unit/UI test execution per repo policy.

* Allow new Claude sessions to replace stopped owners

A stopped Claude turn must keep the same session eligible for the next turn, but it must not strand the workspace if that process exits before session-end. Mark stopped ownership as replaceable and let only session activation paths use that escape hatch; clear-session ownership remains non-replaceable so stale pre-clear events stay blocked. Session-end fallback cleanup now checks the consumed session id so missing input session ids do not fail open.\n\nConstraint: /clear SessionStart must continue to suppress stale pre-clear startup, stop, and session-end events\nRejected: Clear active ownership on Stop | breaks same-session multi-turn prompt-submit currentness\nConfidence: medium\nScope-risk: narrow\nTested: git diff --check; conflict-marker scan\nNot-tested: local XCTest per repo policy; CI will run cmux unit regressions

* test: cover codex hooks TOML features section

* fix: harden hook config and auth token cache

* test: cover sign-out browser auth loading cleanup

* fix: cancel browser auth on sign-out

* test: cover codex config read failures

* fix: fail closed on codex config read errors

* test: cover auth sign-out callback race

* fix: discard auth callback after sign-out

* fix: keep claude subcommands out of hook injection

* fix: dismiss stale sparkle update replies

* fix: redact auth logs while preserving observability

* fix: reset update checks and guard sign-out races

* fix: preserve update metadata from driver state

* fix: await auth token assertions before comparing

* fix: address wrapper and auth review feedback

* fix: address session and config review feedback

* ci: retrigger pending checks

* fix: clear stale ime and auth token state

* fix: clean legacy codex hooks config

* test: cover legacy codex hooks markers

* fix: clean empty codex features table

* fix: preserve browser key reentry dispatch

* Fix new-workspace caller window routing (#4042)

* test: cover new workspace caller routing

* fix: route new workspace to caller context

* Add iMessage workspace insertion regression test

* Reset Kitty keyboard mode at shell prompt boundaries (#3870)

* Prove stale Kitty keyboard state leaks CSI-u key bytes

The regression exercises a hosted Ghostty terminal, leaves Kitty keyboard protocol enabled as a crashed TUI would, mirrors the clear-history socket handler clear_screen path, and captures raw PTY stdin bytes for a plain c key. Current behavior should encode CSI-u instead of a single ASCII byte, making the test fail until the protocol state is reset at the shell prompt boundary.

Constraint: Regression must cover PTY input bytes, not source text shape
Confidence: high
Scope-risk: narrow
Directive: Keep this test on the real ghostty_surface_key path; sendText alone bypasses the keyboard encoder
Tested: Manual current-main repro captured c9;1:3uc9;1:3uc9;1:3u after Kitty enable plus clear-history
Not-tested: Local unit execution deferred to CI per requested red/green workflow

* Reset stale Kitty keyboard mode at prompt boundaries

A crashed TUI can leave Ghostty's Kitty keyboard protocol stack pushed after clear-history, causing normal shell input to be encoded as CSI-u. Resetting the stack from the shell prompt hook makes the prompt boundary the ownership point for returning interactive shells to plain byte input.

Constraint: Fix must run through shell integration because Ghostty keeps protocol state inside the terminal surface

Rejected: Reset only in clear-history socket path | stale state also leaks after any crashed TUI returns to prompt

Confidence: high

Scope-risk: narrow

Directive: Keep prompt-boundary reset paired across bash and zsh integrations

Tested: Not run locally per repository testing policy; regression added in prior commit

Not-tested: CI not yet completed

* Clarify Kitty reset fixture failures

The stale keyboard regression now fails at the fixture boundary if the zsh integration does not emit the expected reset bytes, instead of falling through to a misleading PTY byte mismatch.

Constraint: Repository policy forbids local test execution for this PR loop

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repository policy; CI will run the affected XCTest

Not-tested: Local XCTest execution

* chore: retrigger Vercel checks

* Add Kitty reset shell hook coverage

* Reset all terminal keyboard protocols at prompt

* Fix terminal keyboard reset test expectations

* Clarify disabling agent session auto-resume for #3640 (#3991)

* docs: explain disabling agent auto resume

* docs: name auto resume config path

* Fix stale restored agent resume state

* Harden restored agent hook liveness

* refactor: share restored agent normalization

* Honor iMessage workspace ordering and previews

* Add workspace cwd inheritance setting (#3921)

* feat: add workspace cwd inheritance setting

* fix: align workspace cwd setting key naming

* fix: apply workspace cwd setting to detached creation

* fix: expose workspace cwd inheritance setting

* fix: route pane break through detached workspace creation

* fix: keep pane break response pane ids non-null

* fix: distinguish pane break resolution errors

* Approve installed Codex hooks (#4035)

* Approve installed Codex hooks

* Address Codex hook trust review feedback

* Avoid tomllib in Codex hook tests

* Align Codex hook trust test mirror

* Harden Codex hook trust ownership

* Preserve legacy Codex hook cleanup

* Fix workspace unit test after merge

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Revert "Approve installed Codex hooks (#4035)" (#4074)

This reverts commit e4546b7675a4ffa5a41a5429218b60f4e7644e21.

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Fix workspace unit test transfer resume state (#4076)

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Clarify in cmux --help that reload-config covers Ghostty config too (#4060)

* Clarify cmux help that reload-config covers Ghostty config too

`cmux reload-config` reloads BOTH ~/.config/cmux/cmux.json and Ghostty
config (~/.config/ghostty/config) and refreshes terminals in place, but
the help/docs only mentioned cmux.json. Agents (and humans) reading the
help would think they had to restart cmux after editing Ghostty config.

- cmux --help "Agent Help" now tells agents where Ghostty config lives
  and that reload-config picks it up live.
- cmux docs settings, cmux settings path, cmux config --help now list
  ~/.config/ghostty/config as a related (not cmux-owned) location and
  describe reload-config's actual scope.
- cmux schema sidebarAppearance.tintOpacity description now notes it's
  sidebar-only, and points to Ghostty background-opacity / blur for
  terminal transparency.
- skills/cmux/SKILL.md mirrors the wording.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Align ghostty_config JSON shape across CLI surfaces

Cursor Bugbot and Greptile both flagged that `cmux settings path --json`
emitted `ghostty_config` as a string while `cmux docs settings --json`
emitted it as an object {path, note}. An agent reading both outputs with
the same key expectation would have to special-case the type.

Standardize on the object shape with `path` and `note` in both, matching
docsPayload. This is the agent-discoverability path the PR is trying to
make reliable, so making the shape consistent is on-purpose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use canonical Ghostty config key background-blur (not background-blur-radius)

CodeRabbit flagged that `background-blur-radius` is outdated. Verified
against the Ghostty submodule at ghostty/src/config/Config.zig: line 70
declares `background-blur-radius` as a compatibilityRenamed alias for
`background-blur`. The current canonical key is `background-blur` and it
accepts the same integer value (e.g. `background-blur = 20`).

Update the two docs that introduced the alias name:
- skills/cmux/SKILL.md
- web/data/cmux.schema.json (sidebarAppearance.tintOpacity description)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add nucleo FFI command palette benchmark

* Open right sidebar tools as panes (#4065)

* Open right sidebar tools as panes

* Remove unreachable sidebar pane commands

* Fix sidebar pane unit test build

* Address sidebar pane review feedback

* Fix vault pane focus tracking

* Address right sidebar pane review followups

* Use modern sidebar pane flash observer

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Save crash diagnostics under cmux state (#4077)

* Save crash diagnostics under cmux state

* fix: key GhosttyKit artifacts by crash path

* fix: pin cmux crash GhosttyKit archive

* fix: mark crash breadcrumb scan concurrent

* fix: keep crash scan compatible with Xcode 16

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Menubar global search P1 (#3908)

* Ship local global search as a remappable menubar flow

Phase 1 needs browser and markdown value without terminal scrollback, so the index owns durable FTS5 upserts while AppDelegate keeps one navigation path from palette rows to focused panels. The global shortcut is wired through the existing shortcut settings instead of a standalone Carbon shim so Settings and cmux.json remain authoritative.

Constraint: Phase 1 excludes Ghostty terminal scrollback capture

Constraint: User required no local test execution and no reload before CI is green

Rejected: Hardcoded GlobalSearchHotkey shim | violates KeyboardShortcutSettings policy

Confidence: medium

Scope-risk: broad

Directive: Keep future terminal capture feeding SearchIndex documents through GlobalSearchCoordinator rather than adding another palette/navigation path

Tested: git diff --check; python3 -m json.tool Resources/Localizable.xcstrings; python3 -m json.tool web/data/cmux.schema.json; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Not-tested: Local unit/UI tests and tagged app launch per task constraints

* Remove duplicate search refresh work

Cursor review pointed out that the menubar toggle and palette onAppear both refreshed the live index. Keeping the refresh in the palette lifecycle avoids resetting browser debounce tasks while still indexing each time the palette opens, and removing the unused workspace delete API keeps the storage surface honest.

Constraint: Review feedback came from PR #3908 after the first CI pass started

Rejected: Keep both refresh calls | causes avoidable debounce cancellation and slower browser result availability

Confidence: high

Scope-risk: narrow

Directive: Add workspace-level deletion only when a real workspace teardown caller is wired

Tested: git diff --check; python3 -m json.tool Resources/Localizable.xcstrings; python3 -m json.tool web/data/cmux.schema.json; plutil -lint GhosttyTabs.xcodeproj/project.pbxproj

Not-tested: Local tests/build per task constraints

* Fix global search stale index entries

* Address global search review feedback

* Fix search query token mapping compile error

* Fix global search CI failures

* Fix stale unavailable markdown search entries

* Cancel stale markdown search captures

* Cancel global search refresh on dismiss

* Fix global search review followups

* Address global search post-CI feedback

* Preserve markdown panel title search on read failure

* Address global search lifecycle feedback

* Address global search review lifecycle feedback

* Refine global search capture ownership

* fix: address global search review blockers

* feat: show open panels in global search

* fix: cover right sidebar tool panel in search

* Fix cmuxTests: rename restorableAgentAutoResumePending to restorableAgentResumeState (#4068)

* fix: handle repeated assistant imessage completions

* Open supported files in cmux on cmd-click (#4041)

* Open supported files in cmux on cmd-click

* Add cmd-click file preview verification script

* Reuse right pane for cmd-click file previews

* Keep cmd-click UI test terminal after preview focus

* Accept numeric cmd-click test payload values

* Capture cmd-click UI test window snapshots

* Add file-type-aware external open actions

* Address supported file routing review feedback

* Fix external open menu sendability warnings

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Suppress native proxy icon on cmux main windows (#3973)

* Add proxy icon regression test

* Suppress native proxy icon on cmux windows

* chore: retrigger preview deployments

* Refine native proxy icon suppression

* fix: keep titlebar folder icon aligned

* fix: restore titlebar folder icon leading offset

* test: cover folder icon frame replacement

* fix: resync folder icon on frame replacement

* test: cover folder icon ancestor movement

* fix: track folder icon ancestor movement

* fix: address folder icon review feedback

* test: stabilize folder icon ancestor sync

* fix: handle sidebar tool panels in global search

---------

Co-authored-by: austinpower1258 <austinwang115@gmail.com>

* Close browser panels when pages request window close (#4070)

* Add browser self-close restore regression test

* Close browser panels from WebKit close callbacks

* fix: index right sidebar tool panels as titles

* fix: keep web close callback synchronous

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add notification policy hooks

Merged https://github.com/manaflow-ai/cmux/pull/4054

* Fix sidebar unread badge after re-marking notifications (#4084)

* Add sidebar unread notification regression test

* Keep sidebar notification badge live during menu freeze

* Cover sidebar presentation fallback cases

* Limit Cloud VMs by active provider state (#4046)

* test: cover active vm limit with paused freestyle vms

* fix: enforce cloud vm limits by active state

* fix: parallelize cloud vm status refresh

* chore: update web security dependencies

* fix: handle right sidebar tools in global search

* fix: guard cloud vm status refresh races

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Fix Settings search synonyms (#4082)

* Add settings search synonym regressions

* Fix settings search synonyms

* Add shortcut bindings anchor regression

* Cover clickable PR settings search alias

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add tagged debug CLI helper (#4092)

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add unread defer shortcut (#4086)

* Add unread defer shortcut

* fix: address unread defer feedback

* fix: keep manual unread jump explicit

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Document notification hooks default off

Document that notification hooks are off by default and style the config key in localized docs.

* Approve installed Codex hooks after dogfood (#4075)

* Reapply Codex hook approval changes for dogfood

* Notify on Codex plan input requests

* Handle Codex plan question transcript items

* Address Codex hook review feedback

* Recover malformed Codex hook trust blocks

* Avoid duplicate consecutive cmux hook reinserts

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Document session restore behavior

Adds session restore docs, blog, README updates, and review cleanup.

* Use nucleo for command palette search

* Skip unrestorable Claude startup sessions

PR: https://github.com/manaflow-ai/cmux/pull/4079

* Revert "Suppress native proxy icon on cmux main windows" (#4099)

* Revert "Suppress native proxy icon on cmux main windows (#3973)"

This reverts commit 5048440ff44b4edbe328b65403724ff79476b2e6.

* Fix titlebar proxy icon without detached panel sync

* Apply proxy icon override to fallback config

* Restore key regain redraw invariant

* Tune nucleo palette initialism ranking

* Fix terminal portal resize lag (#4102)

* Fix Korean 2-Set terminal arrows (#4095)

* Add Korean 2-Set arrow IME regression

* Restore Korean 2-Set arrow forwarding

* Restore Zhuyin IME command routing

* Address IME review feedback

* Fix Korean IME regression test compile

* Address IME review follow-ups

* Address Bopomofo preedit review feedback

* Avoid idle Zhuyin key suppression

* Remove dead text input wrapper

* Address IME suppression review feedback

* Fix palette stitched highlight precedence

* Add Codex Teams subagent panes

* Open markdown files in preview panels from cmux open (#4085)

* fix: open markdown files in preview panels

* fix: preserve markdown viewer transparency

* fix: mute markdown open-with header button

* fix: align markdown header controls

* fix: align file header controls

* fix: address markdown review feedback

* test: cover opaque text editor alpha

* fix: align header open fallback

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Add task manager sorting and program aggregates (#4066)

* Add task manager sorting and program aggregates

* Add sorting to cmux top output

* Add flat TSV cmux top output

* Add coding agent task manager totals

* Make task manager program totals payload-backed

* Recognize agent launcher process names in task manager

* Fix task manager test build helpers

* Recognize Claude versioned launcher processes

* Show loading state before task manager sample

* Recognize versioned agent process names

* Use agent totals for task manager hierarchy icons

* fix: address task manager review feedback

* fix: address follow-up task manager review

* fix: address final task manager review

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Keep manual unread sticky until terminal interaction (#4104)

* test: cover sticky manual unread state

* fix: keep manual unread sticky until terminal input

* fix: show workspace manual unread pane ring

* fix: sync manual unread badge on focus changes

* fix: stabilize manual unread representative fallback

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: austinpower1258 <austinwang115@gmail.com>

* Bump version to 0.64.5 (#4107)

* Fix Pi Vault icon and JSONL titles (#4120)

* test: cover Pi JSONL content block titles

* fix: parse Pi JSONL text blocks for Vault titles

* fix: align Pi JSONL title role handling

* fix: require typed text blocks for Pi titles

* Improve Cloud VM error guidance (#4094)

* Improve Cloud VM error guidance

* Address final Cloud VM review feedback

* Narrow Cloud VM sanitizer env var block

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Keep selected workspace visible after sidebar reorders

* Add sidebar scroll regression for workspace move to top

* Reveal selected workspace after sidebar reorders

* Handle right sidebar tool panels in global search

* Test workspace move to top visibility only

* Refine sidebar reorder scroll trigger

* Add move-to-top notification regression

* Skip no-op move-to-top notifications

* Assert no-op move-to-top keeps order

* Require matching manager for reorder scroll

* Scroll selected workspace on index shifts

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Use transparent backgrounds for file preview panels (#4088)

* Handle right sidebar tools in global search browse hits

* Use transparent backgrounds for file preview panels

* Cover panel theme background preservation

* Fix PDF file preview open menu

* Make file open menu button compact

* Fix PDF open menu chrome button

* Fix PDF open-with chrome click target

* Address file preview chrome review feedback

* Fix PDF background cache invalidation

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>

* Inherit stdin in backgrounded ssh inside startup wrapper (#4135)

* Add regression test for SSH startup wrapper dropping stdin

After PR #3786 backgrounded the ssh invocation inside the startup
wrapper for signal/reconnect handling, `cmux ssh <host>` sessions
stopped forwarding keystrokes from the surface PTY to the remote
shell. Output flowed back (the remote prompt rendered in cmux UI),
but anything typed never reached zsh on the other side, which sat
blocked in `do_poll` on the remote pts.

Root cause: POSIX sh redirects stdin of an async command (`&`) to
/dev/null when job control is off — the default for the `/bin/sh -c …`
that runs the startup wrapper. Without an explicit `<&0` on the
`&`'d ssh line, the local PTY stdin is silently dropped.

This commit adds the failing regression test. The fix follows in
the next commit so CI can prove the test catches the bug.

* Inherit stdin in backgrounded ssh inside startup wrapper

PR #3786 wrapped the ssh invocation in `while :; do … & wait` to enable
SIGHUP/INT/TERM trap handling and reconnect-on-exit-255. Backgrounding
ssh, however, drops its stdin: POSIX sh redirects fd 0 of any async
command to /dev/null when job control is off, which is the default for
the `/bin/sh -c …` host that runs this wrapper.

As a result, output from the remote still flowed back to the surface
PTY (ssh's stdout/stderr stayed wired) but the user's keystrokes never
reached the remote — they hit ttysNNN on the Mac side, kernel echoed
them locally, but ssh's stdin was /dev/null so nothing was forwarded.
zsh on the far side sat in `do_poll` forever.

Explicit `<&0` on both the `command` line and the `( … )` shell-snippet
form overrides the POSIX default and re-attaches the wrapper's own
stdin to the backgrounded ssh process.

Verified with the regression test added in the previous commit, plus
manual repro on Darwin:

    /bin/sh -c 'cat &  wait'           # cat's fd 0 → /dev/null (bug)
    /bin/sh -c 'cat <&0 &  wait'       # cat's fd 0 → parent stdin (fix)

* Add docs search

Adds localized Pagefind docs search with heading anchors and section-aware results.

* Fix Swift interpolation escape in SSH stdin regression test (#4154)

`testSSHStartupForwardsStdinToBackgroundedSSH` used `\"<empty>\"` inside a
`\(...)` string interpolation. Swift parses `\"` as the end of the outer
literal, breaking compilation of cmuxTests on main:

    Cannot find ')' to match opening '(' in string interpolation
    Unterminated string literal

That kept `ci/circleci: macos-unit-tests` red on main after #4135 even
though `macos-debug-build` and `macos-release-build` still passed (the
test target was the only thing affected). The runtime fix (`<&0` on the
backgrounded ssh) is unchanged and was verified end-to-end on a cloud Mac.

Inside an interpolation, the string is already a Swift expression and
literal quotes are unescaped; drop the four backslashes.

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add command palette settings toggles

Adds command palette toggles for boolean Settings rows, including iMessage Mode.

* Ensure Rust toolchain is installed for nucleo FFI builds

* Document nucleo FFI thread and ABI assumptions

* Reuse nucleo index in preview search test helper

* Install Rust for activation perf builds

---------

Co-authored-by: Tobi Lutke <tobi@shopify.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kevin Peng <48529172+kays0x@users.noreply.github.com>

* Address command palette review feedback

* Address command palette review follow-ups

* Fix command palette preview responsiveness

* Build command palette search index off main actor

* Keep small cold palette searches synchronous

* fix: clear panel badges when marking notifications read

* fix: preserve nucleo normalized matches

* fix: preserve typo fallback with nucleo search

* fix: keep search fallback semantics

* fix: keep nucleo aliases authoritative

* fix: preserve typo fallback without ffi contention

* fix: avoid stale palette reset snapshot

* fix: narrow nucleo typo fallback

* fix: address command palette review bots

* fix: preserve palette activation during index refresh

* test: cover nucleo multi-token field matching

* fix: tokenize nucleo ffi queries

* fix: preserve palette activations during index refresh

* fix: sync palette pending state before async search

* fix: keep long keyword matches below title matches

* fix: keep nucleo fuzzy matches within fields

* fix: guard palette preview reuse by fingerprint

* test: skip optional nucleo bundle check without cargo

* fix: keep final palette results uncapped

* fix: respect optional nucleo fallback

* fix: initialize rustup toolchain in release workflow

* fix: run pending palette activation after sync refresh

* Fix command palette duplicate ID indexing

* Remove stale command palette label helpers

---------

Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
Co-authored-by: Tobi Lutke <tobi@shopify.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kevin Peng <48529172+kays0x@users.noreply.github.com>
lawrencecchen added a commit that referenced this pull request Jun 5, 2026
PR #3908 changed the cmux menu bar status item so a left-click opened the
720x460 global search popover, and only a right/control-click showed the
dropdown menu. This reverts the status item to its standard behavior: any
click shows the dropdown menu again.

Global search stays reachable via the "Search All Windows…" menu item and
its keyboard shortcut, so nothing is lost. The change just restores
statusItem.menu = menu and removes the custom button click routing added
by #3908.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Supersynergy

Copy link
Copy Markdown

Thanks for building this, @austinywang! 🙏 Shipped exactly what I had in mind when I opened #3865 — local FTS5, sub-100ms, menubar-anchored. The remappable shortcut path (instead of a hardcoded Carbon hotkey) was the right call. Looking forward to the P2 terminal-scrollback work in #4171.

mattpetters added a commit to mattpetters/cmux that referenced this pull request Jun 12, 2026
…low-ai#5851) (#7)

* ci: publish iOS TestFlight (beta) on iOS-affecting merges to main (#5453)

Add a push trigger so every merge to main that changes the iOS app publishes
to the beta lane (dev.cmux.app.beta) immediately, instead of waiting up to a
day for the nightly. Path-filtered to inputs that actually rebuild the iOS app
(ios/, the linked Swift packages, GhosttyKit + its fetch scripts, and this
workflow); macOS-only Sources/ changes don't change the iOS app so they don't
trigger an upload. The nightly + manual dispatch stay as-is. Push runs always
build (the dedup SHA gate is schedule-only); each merge is a distinct commit,
so no duplicate uploads.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Restore menu bar icon dropdown menu on click (#5451)

PR #3908 changed the cmux menu bar status item so a left-click opened the
720x460 global search popover, and only a right/control-click showed the
dropdown menu. This reverts the status item to its standard behavior: any
click shows the dropdown menu again.

Global search stays reachable via the "Search All Windows…" menu item and
its keyboard shortcut, so nothing is lost. The change just restores
statusItem.menu = menu and removes the custom button click routing added
by #3908.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Mobile workspace list: propagate renames + match bonsplit terminal order (#5446)

* test: add failing mobile workspace-list fidelity tests

Adds MobileWorkspaceListFidelityTests (behavioral) covering the two bugs:
a pure terminal reorder must wake the observer and change the mobile
summary hash, and a terminal rename (which writes panelCustomTitles) must
change the hash. These fail against current behavior:

- mobileTerminalPanels orders focused-first/UUID, not spatial order
- the observer never subscribes to reorder/custom-title changes and hashes
  the sorted panel-id set + raw panelTitles, so reorders and custom renames
  don't re-emit to the phone

Only the structural seam the tests need to compile is added here:
Workspace.orderedPanelIds (spatial order from bonsplit) and the
Workspace.paneLayoutVersion counter. The behavioral wiring lands in the
next commit, so CI goes red here and green there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: mobile workspace list reflects renames + spatial terminal order

Serialize the phone's terminal list in the on-screen bonsplit spatial
order (left-to-right, top-to-bottom) and re-emit workspace.updated on
terminal renames and pure drag-reorders, fixing the two regressions the
prior commit's tests exercise.

- mobileTerminalPanels(in:) now routes through orderedPanels(in:), which
  delegates to Workspace.orderedPanelIds (bonsplit allTabIds order), instead
  of focused-first/UUID sort. The payload still carries is_focused.
- MobileWorkspaceListObserver subscribes to $panelCustomTitles (terminal
  rename writes panelCustomTitles, not panelTitles) and $paneLayoutVersion
  (reorder wakeup), and hashes the ordered panel-id sequence + custom-aware
  panelTitle() instead of the sorted id set + raw panelTitles.
- Workspace.didChangeGeometry bumps paneLayoutVersion only when orderedPanelIds
  actually changed, so divider drags and selection-only events stay quiet.

Workspace rename already propagated (setCustomTitle sets title; observer
watches $title; response sends workspace.title) and needs no change.
Host-only serialization change; no iOS app rebuild required.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: move fidelity tests to a dedicated Swift Testing file (cmux policy)

cmux test-framework policy: new non-UI tests use Swift Testing, not XCTest.
The fidelity tests were appended to the XCTest file WorkspaceUnitTests.swift;
move them to a dedicated cmuxTests/MobileWorkspaceListFidelityTests.swift
written in Swift Testing (#expect/#require/@Test/@Suite(.serialized)), wired
into the cmux-unit target via project.pbxproj. WorkspaceUnitTests.swift is
restored to base (its XCTest import stays only for its pre-existing suites,
which we must not bulk-rewrite). Same behavioral assertions; the prior two
commits keep the XCTest red/green proof in history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: keep orphan panelDirectories in mobile summary hash

The prior hash change dropped the loop that hashes every panelDirectories
entry (including ids not yet in `panels`), which broke the pre-existing
testMobileWorkspaceListHashIncludesDisplayedDirectories (it sets a directory
for an orphan UUID and expects the hash to change). Restore that loop; the
bug-fix changes (ordered panel ids + custom-aware panelTitle) stay. Keeps the
existing behavior where a directory update is detected before its panel
registers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Add VS Code-style context keys and comparison operators to shortcut `when` clauses

Generalize the keyboard-shortcut `when` engine (#5189) from four boolean focus
atoms to a typed, extensible context system, closing most of the gap with VS
Code's `when`-clause vocabulary while staying fully backwards compatible.

- Operators: add ==, !=, =~ (regex), <, <=, >, >=, and `in [a, b]`, layered into
  the grammar with VS Code precedence. Existing boolean clauses parse identically.
- Context keys (typed registry ShortcutContextKnownKey): commandPaletteVisible,
  terminalFindVisible (bool), sidebarMode (string), paneCount, workspaceCount (int),
  wired from synchronous window state in KeyboardShortcutContext.
- Typed model: ShortcutContext / ShortcutContextValue / ShortcutContextOperand /
  ShortcutRegex value types; the app populates a Sendable snapshot so the package
  never imports app types.
- canCoexist (conflict detection) generalized to a sound free-variable enumeration:
  byte-identical for focus-only clauses, conservative for typed comparisons.
- ShortcutWhenClause stays additive (.key/.compare added; .atom and the
  evaluate(ShortcutFocusState) overload unchanged). One intentional change: an
  unknown bare key now parses to .key (always-false), matching VS Code.
- Treat an empty/whitespace `shortcuts.when` clause as non-restricting so it no
  longer suppresses an action's menu equivalent.
- Package Swift Testing suite + app integration test; docs updated in
  cmux.schema.json and the en/ja message catalogs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Browser omnibar: first focus-gaining click selects whole URL (Chrome parity)

Match the Chrome/Safari/Arc omnibar click model exactly:
1. The first click on an unfocused omnibar showing a URL selects the
   entire contents, so the next keystroke replaces the URL.
2. A subsequent click while the omnibar is already first responder places
   the caret at the click point (preserves issue #5268).
3. A double-click selects the word under the cursor (unchanged
   field-editor behavior).

The mouse-gesture state machine in OmnibarNativeTextField already owns the
mouse selection decision, so the change stays there rather than routing a
mouse click through the async, notification-based requestAddressBarFocus
selection-intent path (which is for keyboard/programmatic focus like
Cmd+L). MouseSelectionState now records whether the click gained focus and
whether Shift was held; mouseUp consults the pure, testable decision
function browserOmnibarFocusGainingClickShouldSelectAll to select all only
on an undragged, unmodified focus-gaining click. Drags and Shift-clicks
keep their explicit range; double-clicks never reach this path.

Closes #5459

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: keep restored hidden webview attached during capture

* test: cover OMO tmux respawn panes

* Omnibar: count UTF-16 length without NSString bridge

Address Greptile P2: use editor.string.utf16.count for the select-all
range length instead of bridging to NSString just to read .length. Same
value, no Obj-C bridge cast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Scale browser chrome with the tab bar font size

The browser omnibar text and toolbar icon buttons (back/forward/reload,
lock, screenshot, cursor grab, profile, theme, dev tools) were a fixed
size and ignored the user's font settings, so the top chrome looked
inconsistent once the tab bar font size was changed.

Derive every omnibar/toolbar size from the existing `surfaceTabBarFontSize`
setting via a new pure `BrowserChromeMetrics` value type: a scale anchored
to the shipped default (11pt) so the default appearance is byte-identical,
clamped to a sane range so a malformed config can't blow up the toolbar.
BrowserPanelView seeds the size from the cached config and refreshes it
live on `.ghosttyConfigDidReload` — the same observation path the tab strip
and terminal panels already use — so the chrome re-lays-out the instant the
tab bar font size changes. No new setting is introduced.

Closes #5463

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: respawn OMO subagent panes

* fix(tests): pass fontSize to OmnibarTextFieldRepresentable test constructions

The new required fontSize parameter on OmnibarTextFieldRepresentable broke
two existing test-target call sites, failing the tests job to compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: align tmux respawn pane semantics

* fix: retain browser screenshot URL waiter

* test: cover -infinity and clarify min-font comment (Greptile)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: expose terminal wait-after debug state

* Add failing main window min-size regression test

* Clamp main window AppKit fitting size to policy floor

* fix: keep respawned tmux panes attached

* fix: tighten tmux respawn semantics

* test: isolate omo respawn assertions

* fix: clear respawn warning budget

* fix: preserve respawn startup environment

* Fix SIGTRAP when ASWebAuthenticationSession completes off the main thread

The completion closure handed to ASWebAuthenticationSession was formed
inside the @MainActor factory, so under the package's Swift 6 language
mode it inherited main-actor isolation and the compiler emitted a
dynamic isolation assertion at the ObjC boundary. macOS 26 delivers the
session's cancel-path completion on the SafariLaunchAgent XPC queue (the
deleted AuthManager's Swift 5 app-target code documented this off-main
behavior but emitted no check), so dismissing the sign-in popup trapped
in dispatch_assert_queue.

The completion is now built by a nonisolated @Sendable bridge that
carries no isolation assumption and hops to the main actor itself. A
true red/green regression commit is not practical here: the trapping
closure was only reachable through a live ASWebAuthenticationSession
callback, and the trap is a compiler-inserted assertion, not logic the
old shape exposed to tests. The new tests pin the bridge's contract by
delivering the completion from non-main queues.

Part of https://github.com/manaflow-ai/cmux/issues/5375

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Extract SocketControlServer into CmuxControlSocket (stage 2) (#5432)

* Extract SocketControlServer into CmuxControlSocket (stage 2)

Move the control-socket listener out of TerminalController into the
CmuxControlSocket package: startup path reservation, bind/listen
lifecycle, the generation-counted accept source with failure backoff
and rearm, the socket-path monitor, and synchronous state reads. The
former nonisolated(unsafe) field block and NSLock collapse into one
OSAllocatedUnfairLock-guarded state machine (documented carve-out: all
drivers are synchronous - DispatchSource handlers, client reader
threads, and app-termination teardown that must finish before exit).

App-shaped concerns cross a closure seam (SocketControlServerEvents):
telemetry breadcrumbs/failures with the existing capture cooldown,
the .socketListenerDidStart notification + PortScanner wiring at the
same point in start, accepted-client hand-off to the existing
thread-per-client handler, and the path-missing/rearm restart
triggers, which keep their main-thread scheduling in the app.

Also moved: SocketFastPathState (DispatchQueue-as-lock -> lock-guarded
package type keyed on raw state strings; dead shouldPublishDirectory
dropped) and the peer PID/UID/ancestry checks (SocketTransport+Peer).

TerminalController keeps a thin facade with unchanged signatures, the
client read loop, auth, and command dispatch (those move in stage 3).
All telemetry stage strings unchanged. -855 net lines.

20 new package tests (real-socket lifecycle, crash-reclaim stale
socket replacement, reservation consumption, path-monitor delete
detection, per-mode permissions, dedupe cache, peer verification);
65 total green.

* review: rename print prefixes, asyncAfter justification, DocC examples

- print() prefixes in SocketControlServer+Startup say SocketControlServer
  instead of the legacy TerminalController name (stdout prints kept for
  launch-time visibility parity with the legacy listener).
- One-line justification comment on the accept-source resume asyncAfter
  (bounded backoff deadline in a non-async type; stale fires are no-ops
  via the generation/identity/suspended guards).
- DocC usage examples + cross-references on SocketTransport peer APIs
  and SocketFastPathState.

* Make the session completion bridge an instance method

nonisolated on the instance method escapes the factory's @MainActor
isolation just as well as static did, and the bridge can use the
instance's own log instead of taking it as a parameter.

Part of https://github.com/manaflow-ai/cmux/issues/5375

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: harden iOS TestFlight publish (main-only guard + CODEOWNERS) (#5476)

Defense-in-depth for the publish pipeline:
- Upload job gains `github.ref == 'refs/heads/main'`, so a publish can only run
  from main. push/schedule already run on main; this blocks shipping arbitrary
  code by dispatching the workflow against a feature branch.
- Add .github/CODEOWNERS for secret-touching paths (all workflows, the
  upload-testflight.sh publish script, ios/Config) so changes there require an
  owner's review.

NOTE: CODEOWNERS only enforces once branch protection on main sets
require_code_owner_review=true (+ required_approving_review_count>=1). That
branch-protection change is the actual gate and is an admin action; these two
changes make it effective and add depth, they are not the gate themselves.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address omo respawn review feedback

* Fold AuthErrorMapper into AuthError

The mapper was a stateless value whose whole job was translating raw
backend errors into the AuthError vocabulary; that conversion now lives
on the type itself as AuthError(displaySafe:) (failable: nil means the
original Stack error is already display-safe and the sign-in UI renders
it unchanged), with the cached-session recovery decision as a property.
The StackAuth-dependent conversion sits in AuthError+DisplaySafe.swift
so AuthError.swift stays Foundation-only.

Part of https://github.com/manaflow-ai/cmux/issues/5375

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Organize CMUXAuthCore: one type per file, DocC, no namespace-enums

Every public symbol now carries DocC, matching CmuxAuthRuntime, and
every type has its own file (CMUXAuthEnvironment, the key-value store
protocol, and the launch-input types move out of shared files). The two
namespace-enums become real shapes the conventions allow:

- CMUXAuthLaunchConfig's parsers are now failable initializers on the
  values they construct: CMUXAuthAutoLoginCredentials(environment:...)
  and CMUXAuthUser(uiTestFixtureEnvironment:...).
- CMUXAuthMagicLinkCode is a value (code + nonce) with a composed
  property instead of a caseless enum with a static compose.
- CMUXAuthConfig.resolve and AuthConfig.resolve become initializers.

Part of https://github.com/manaflow-ai/cmux/issues/5375

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ios: name the TestFlight beta "cmux BETA" on device (#5485)

Release builds ship the beta lane (dev.cmux.app.beta) but displayed as plain
"cmux", indistinguishable from a future App Store "cmux". Override
PRODUCT_DISPLAY_NAME = "cmux BETA" in Release.xcconfig (after the Shared.xcconfig
include, so it wins). Debug builds (dev.cmux.ios) stay "cmux".

Verified deterministically: Release config's base is Release.xcconfig (pbxproj
baseConfigurationReference), there is no direct PRODUCT_DISPLAY_NAME in the
pbxproj, and INFOPLIST_KEY_CFBundleDisplayName = $(PRODUCT_DISPLAY_NAME), so the
Release app's CFBundleDisplayName resolves to "cmux BETA". Matches the macOS
"cmux NIGHTLY" / "cmux DEV" variant-naming convention.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix: address main window sizing review feedback

* test: address main window sizing review comments

* Remove mobile host legacy defaults (#5484)

* Fix notification sound selection playback (#5480)

* Fix notification sound selection playback

* Address notification sound staging review

* Fix sidebar close button hidden under wrapped workspace titles (#5488)

The workspace row's close (x) button was a floating
overlay(alignment: .topTrailing) that reserved no layout space, while
the title used frame(maxWidth: .infinity) with no trailing inset. A
title long enough to wrap (or any long single-line title) therefore
filled the top-right corner, and the semi-transparent x rendered on top
of the title glyphs with no background, so it read as missing. Short
titles left that corner empty, which is why single-line names looked
fine.

Move the close button into the title HStack as a trailing sibling that
always reserves its width when the workspace is closable, toggling
visibility via opacity (so hover never re-lays-out the row). The title
now wraps/truncates before the button's corner, leaving a clear area
where the x always shows. This matches the existing group-header
plus-button pattern.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci(ios): archive unsigned, sign only at export (fix TestFlight cert-cap failure) (#5496)

* ci(ios): archive unsigned, sign only at export (fix dev-cert churn)

Automatic signing during `xcodebuild archive` (-allowProvisioningUpdates +
CODE_SIGN_STYLE=Automatic) makes each ephemeral CI runner mint a new Apple
Development certificate, which exhausted the account's certificate cap and
broke every on-merge TestFlight publish ("maximum number of certificates" /
"no profiles for dev.cmux.app.beta"). Archive without signing; the export step
applies the (reused, cloud-managed) distribution cert, which does not churn.

Includes a TEMP push trigger + ref-guard relaxation for this branch to
validate a real upload before merge; both reverted before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci: remove temp branch validation hooks (archive-no-sign verified)

Validated on-branch: archive unsigned + cloud-distribution export uploaded
cleanly (UPLOAD SUCCEEDED, Delivery UUID 2f8bd406..., build 202606060130),
no new dev cert minted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): 14-digit build number so TestFlight offers updates (#5499)

CFBundleVersion regressed from 14-digit (yyyyMMddHHmmss, e.g. 20260520031606)
to 12-digit (yyyyMMddHHmm, e.g. 202606060220). Numerically the 12-digit values
(~2.0e11) are LOWER than the legacy 14-digit ones (~2.0e13), so every recent
build sorted BELOW the May builds and TestFlight never offered an Update (it
picks the highest CFBundleVersion as "latest"). Restore seconds so build
numbers exceed the legacy max and increase monotonically.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): 14-digit build number in the workflow fallback too (#5503)

PR #5499 fixed the script default, but the workflow's "Resolve build number"
step ALWAYS passes --build-number explicitly, and its no-input fallback was
still 12-digit (date -u +%Y%m%d%H%M). So every automatic push/schedule build
overrode the script's 14-digit default with a 12-digit value, leaving the
CFBundleVersion below the legacy max (20260520031606) and TestFlight never
offered it as an update. Match the script: yyyyMMddHHmmss.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): enforce monotonic TestFlight build numbers against App Store Connect (#5504)

* ci(ios): enforce monotonic TestFlight build numbers against App Store Connect

The build-number scheme regressing from 14 to 12 digits silently shipped builds
with a CFBundleVersion below the existing max, so TestFlight never offered them
as an update (it ranks the highest build number as "latest"). Restoring the
scheme (#5499, #5503) fixed the symptom but nothing enforced the actual
invariant, and a bad manual --build-number would reproduce it.

Add a behavioral guard: before archiving, asc_max_build.py asks App Store
Connect for the current max integer CFBundleVersion (mints an ES256 JWT, resolves
the app by bundle id, pages builds and maxes as int because ASC sort=-version is
a string sort). upload-testflight.sh self-heals BUILD_NUMBER up to max+1 when it
would not be the highest, with a loud warning.

Fail-open by design: any ASC/network/JWT error (or missing `cryptography`) logs a
warning and keeps the timestamp build number, so a transient API hiccup never
blocks a publish. The workflow best-effort installs `cryptography` for the guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): address autoreview on build-number guard

- Reused archives (--archive-path): the embedded CFBundleVersion ships, not the
  shell BUILD_NUMBER, so the guard now reads the archive's CFBundleVersion and
  fails (re-archive needed) instead of self-healing a value that won't apply.
  Skipped for --export-only (no upload).
- Supply chain: install `cryptography` BEFORE the App Store Connect private key
  is written to disk, and pin to a wheel-satisfiable range, so a compromised
  install can't read the signing credential.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): never return a partial App Store Connect build max

Page until ASC stops returning a `next` link instead of capping at 20 pages and
returning whatever was seen so far. A truncated read could be below the true max,
letting the caller self-heal to a number still <= the real max (the exact
non-updatable build this guard prevents). MAX_PAGES is now only a runaway
backstop; hitting it with more pages pending raises, so the caller fails open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): address PR-bot findings on the build-number guard

- Invoke asc_max_build.py via explicit `python3` (not a bare path), so a lost
  exec bit can't silently make the guard always fail open. (cursor)
- Reused --archive-path: require a NUMERIC embedded CFBundleVersion; if it can't
  be read, skip the guard with a warning instead of falsely "bumping" a value
  that never applies to the archive. (cursor)
- asc_max_build.py: raise on a `next` URL that doesn't start with API_BASE, so a
  malformed pagination link can't silently truncate to a partial max. (CodeRabbit, cursor)
- asc_max_build.py: emit only HTTP status + ASC error code, never the raw
  response body, to keep upstream payloads out of logs. (CodeRabbit)
- asc_max_build.py: drop the ambiguous saw_any flag; return highest (0 = no
  floor). (greptile)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): report the post-guard CFBundleVersion in the workflow summary

After the monotonic guard self-heals BUILD_NUMBER, the workflow summary was still
printing the pre-guard value, so a release audit could show a CFBundleVersion
that doesn't match the uploaded IPA. The script now writes the shipped build
number to CMUX_BUILD_NUMBER_OUT_FILE (the archive's embedded version for reused
archives), and the workflow reads it into a step output the summary consumes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): remove PyPI dep from signing job; fail closed on reused archives

Addresses two autoreview findings on the build-number guard:

- Supply chain: drop the `cryptography` dependency entirely. asc_max_build.py now
  mints the ES256 JWT via `openssl` (preinstalled) + stdlib, so the TestFlight
  job that holds the signing/upload credential never `pip install`s third-party
  code that could persist in site-packages and run once the key is on disk. The
  workflow's "Ensure cryptography" install step is removed.
- Reused --archive-path can't be renumbered, so it must be verifiable before an
  upload: fail CLOSED when its embedded CFBundleVersion is unreadable or when App
  Store Connect can't be reached, instead of fail-open. Fresh builds keep
  fail-open (the timestamp scheme is already correct). --export-only never
  uploads, so it only warns.

Verified: openssl-signed JWT authenticates to ASC (both key-path and base64-key
paths); all guard branches (fresh fail-open, reused fail-closed x3, export-only
skip) behave correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): close reused-archive verification hole on the Apple ID upload path

The reused-archive fail-closed guarantee only held when ASC API creds gated the
guard block. A reused --archive-path uploaded via the Apple ID/app-specific-
password path (no ASC creds) skipped the block and shipped unverified. Track
REUSED_ARCHIVE_VERIFIED (set only after a real ASC max comparison) and refuse the
upload for any reused archive that reaches it unverified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): fail closed for explicit build numbers when ASC can't be verified

Fail-open is only safe for the generated UTC timestamp (monotonic by
construction). An explicit --build-number could be stale, so if App Store Connect
can't be reached the guard now fails closed for explicit values while keeping
fail-open for the generated default.

To make the distinction real, the workflow no longer passes --build-number for
push/schedule runs: the script generates the timestamp itself (single source of
the numbering scheme, removing the workflow/script duplication that caused the
12-vs-14-digit regression). --build-number is passed only for a manual
workflow_dispatch build_number input, which is exactly the explicit case that now
fails closed when unverifiable. The summary reads the shipped number back from
CMUX_BUILD_NUMBER_OUT_FILE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): close explicit-build-number bypass on the Apple ID upload path

Symmetric to the reused-archive fix: an explicit --build-number uploaded via the
Apple ID path (no ASC API creds) skipped the guard and could ship a stale build.
Generalize the verification flag (REUSED_ARCHIVE_VERIFIED -> GUARD_VERIFIED, set
only after a real ASC max comparison) and the final pre-upload gate now refuses
BOTH an unverified reused archive AND an unverified explicit --build-number. The
generated UTC timestamp stays exempt (monotonic by construction, fail-open).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(ios): only mark a build number verified after a real numeric comparison

GUARD_VERIFIED was set as soon as asc_max_build.py succeeded, before checking
that both values are numeric. A non-numeric explicit --build-number then fell to
the "keep" branch already marked verified, bypassing the fail-closed gate.

Restructure: set GUARD_VERIFIED only inside the numeric branch, after a real
comparison. Non-numeric or unreadable ASC max leaves it unset. Consolidate the
fail-closed check into one gate run BEFORE the archive (fail fast): an unverified
reused archive or explicit --build-number is refused; the generated UTC timestamp
stays exempt (fail-open). Verified-but-stale explicit values now also fail with a
clear message instead of being silently bumped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* iOS pairing onboarding: clearer auth error + Download via TestFlight link (#5506)

* iOS pairing: honest copy for generic auth failure

The Mac collapses every non-account-mismatch authorization failure (invalid
token, wrong Stack project/environment, missing local user, timeout) into a
single `unauthorized` code, which the phone maps to `.authorizationFailed`. Its
copy asserted "Sign in on your computer with the same account…", which is a
specific (often wrong) cause. The most common trigger in practice is a
dev-vs-prod Stack project mismatch (same email, different per-project user ID),
where the token simply can't be verified against the Mac's project.

Reword to state the actual condition without blaming the Mac's account, and hint
at the build/environment cause. The distinct `account_mismatch` path keeps its
accurate "different cmux account" message.

en + ja updated in ios Localizable.xcstrings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* iOS pairing onboarding: "Download via TestFlight" link

Add a "Download via TestFlight" link to both iOS onboarding surfaces — the
"No devices" empty state (DisconnectedWorkspaceShellView) and the Add device
screen (PairingView) — pointing at the Founders Edition page
(https://github.com/manaflow-ai/cmux#founders-edition) since TestFlight is
invite-only for now. The Founders Edition page covers both TestFlight
enrollment and the Mac download.

en + ja added in ios Localizable.xcstrings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add macOS iOS pairing/onboarding window (#5493)

* Add macOS iOS pairing/onboarding window

Adds a dedicated Mac-side window for pairing an iPhone: a scannable QR code
(with a host:port fallback), step-by-step instructions, and live pairing-host
state. Opening it auto-enables the iOS pairing listener, mints a short-lived
attach ticket, and renders the code. Entry point is a "Pair a Device" button in
Settings → Mobile.

- Sources/Mobile/Pairing/: MobilePairingWindowController, MobilePairingView,
  MobilePairingModel, MobilePairingQRImageView.
- MobileHostService.ensureListeningAndReady(): one async entry that starts the
  listener and resolves on readiness via a continuation drained from the
  existing listener-state handlers (no polling; no changes to the accept path).
- SettingsHostActions.openMobilePairingWindow() seam + host implementation;
  MobileSection gains the Pair a Device row.
- 17 strings localized in en + ja.

Revoke and a connected-device list are deferred to a follow-up: there is no
per-device identity on the Mac today to revoke against. Design in
cmuxterm-hq plans/feat-ios-device-revoke/DESIGN.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pairing window: sign-in gate + Tailscale guidance

Restructure the pairing window into a requirements flow (sign in → Tailscale →
QR), since both gate whether a phone can actually pair:

- Sign-in gate: check AuthManager first. When signed out, show a Sign In button
  (beginSignInAndAwait) and don't enable the listener or show a code. When
  signed in, show "Signed in to cmux" with the account email, then prepare a
  code. Authorization is a Stack same-account check, so the Mac must be signed
  in for any phone to pair.
- Tailscale guidance: a requirements row driven by real reachability. The route
  resolver only publishes Tailscale routes (plus DEBUG loopback), so a real
  iPhone needs Tailscale. When no Tailscale route resolves, show a warning + a
  "Get Tailscale" link and note both devices need it on the same account; when
  it resolves, show "Reachable over Tailscale" and the host:port.
- 10 new strings localized in en + ja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Adapt pairing window to AuthCoordinator; add TestFlight link

Rebased onto main; #5387 replaced AuthManager with an injected @Observable
AuthCoordinator + HostBrowserSignInFlow. Migrate the pairing model/view:

- Read isAuthenticated / currentUser / awaitBootstrapped from
  AppDelegate.shared.auth.coordinator; trigger sign-in via
  browserSignIn.beginSignIn() (fire-and-forget). The view re-runs refresh() on
  the coordinator's isAuthenticated and the browser flow's isSigningIn settling
  (handles cancel without spinning).
- Resolve rebase conflicts in MobileHostService (keep the new auth property +
  the readiness continuation) and SettingsHostActions (keep both
  openMobilePairingWindow and the new previewNotificationSound signature).

Also add a "Download via TestFlight" link under the install step pointing at the
Founders Edition page (TestFlight is invite-only for now). en + ja added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pairing window: handle no-Tailscale-route as guided state

Autoreview caught that on a release Mac with no Tailscale address,
createAttachTicket throws noRoutes (release has no debug loopback route) and the
catch showed the raw enum text. Add a dedicated `needsTailscale` state: guard
before minting when status.routes is empty, map noRoutes/routeUnavailable in the
catch, and replace the raw String(describing:) fallback with localized copy. The
state renders "no Tailscale address… install Tailscale, then refresh" with a Get
Tailscale button, and the Tailscale checklist row shows the warning. en + ja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pairing window: auto-refresh QR before ticket expiry

Autoreview caught that the 600s attach ticket could expire while the window
still showed the QR with a perpetual "Waiting…", so a delayed scan would fail.
Schedule a bounded, cancellable re-mint ~30s before TTL elapses (cancelled on
each refresh and on window close via onDisappear), keeping the displayed code
always valid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Settings search: index the Pair a Device row

Autoreview noted the new Settings → Mobile "Pair a Device" row wasn't reachable
from Settings search (search indexes only curated entries). Add a curated entry
(id pairDevice) with pairing/QR/scan/iPhone/iPad/Tailscale/onboarding synonyms,
matching the row's setting:mobile:pairDevice anchor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Tests: add pairDevice to settings reachability contract

The new curated pairDevice search entry is an action row (no cmux.json path), so
SettingsRowAnchorResolutionTests.everyCuratedSettingEntryIsReachable would flag
it unreachable. Add setting:mobile:pairDevice to explicitlyAnchoredEntryIDs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pairing window: address review findings + fix warning budget

- Fix tests-build-and-lag warning budget: MobileHostService.shared was read from
  a nonisolated default-arg context; resolve `.shared` in the @MainActor init body
  (host: MobileHostService? = nil).
- Serialize refresh() with a generation guard so a slower in-flight run can't
  overwrite a newer ticket (race flagged by Cursor + CodeRabbit).
- Stop rendering the raw NWListener error string; show localized listener-offline
  copy (CodeRabbit).
- Deminiaturize a reused pairing window before bringing it front (CodeRabbit).
- Accessibility label on the QR placeholder (CodeRabbit).
- Drain readiness waiters if the ephemeral-fallback bind fails synchronously, so
  ensureListeningAndReady() doesn't wait the full deadline (CodeRabbit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover OSC 11 socket input preservation

* fix: preserve OSC terminal control sends

* iOS: remove dead TerminalArrowNubView.Direction.escapeSequence (#5505)

The arrow nub drives repeats through TerminalArrowRepeatService ->
TerminalKeyEncoder; the hardcoded escapeSequence property duplicated those
bytes and was never referenced (grep-confirmed zero call sites). Drop it so
TerminalKeyEncoder stays the single source of truth for arrow encoding.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make iOS pairing port configurable with live bound-port status (#5489)

* Make iOS pairing port configurable with live bound-port status

Settings > Mobile gains a configurable pairing-listener port
(mobile.iOSPairingHost.port, default 58465). The port is a
preference: the listener still falls back to an OS-assigned
ephemeral port when it's in use, and the iOS pairing payload uses
the actual bound port, so pairing survives a fallback. A live
bound-port indicator shows the real port and warns when it differs
from the configured one, so a configured port can't silently fail.

Also adds a Mac display-name override (mobile.iOSPairingHost.
displayName) and read-only diagnostics (connected-device count +
reachable routes) while pairing is enabled.

The listener reconciles on settings change through a pure,
unit-tested syncDecision (start/stop/restart only when the enabled
state or port actually changes), so unrelated UserDefaults writes
never drop active iOS connections. Live status reaches the
Foundation-only settings package via new SettingsHostActions seams
backed by a mobileHostStatusDidChange notification.

Adds curated search entries + aliases for the new settings and
en/ja localization for all new strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Mobile settings: surface out-of-range port + show resolved name placeholder

An out-of-range port (e.g. 99999) clamps to the default internally, so
without this it would render a reassuring green "Listening on <default>"
with no hint the typed value was ignored. Show an explicit orange
"Port must be between 1 and 65535." instead (even while pairing is off).

Use the resolved system name as the Display Name field placeholder when
no override is set, so the user sees the actual default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix Swift warning budget: bridge pairing-status notification via Void signal

The MainActor for-await over NotificationCenter.notifications(named:)
tripped the warning budget (non-Sendable Notification crossing isolation
in next()). Mirror UserDefaultsSettingsStore.values(for:): a block
observer yields to a Sendable AsyncStream<Void>, and the MainActor drain
task reads the snapshot, so Notification never crosses. Behavior is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix stale connected-device count: notify on registry mutation, not accept

beginConnection() fires at accept time, before the connection is inserted
into MobileHostConnectionRegistry, but the status snapshot's
activeConnectionCount reads that registry. The status stream could yield
the old count and then never update after the insert. Post
mobileHostStatusDidChange from the registry's insert/remove/removeAll
(where the authoritative count changes) instead.

Addresses autoreview finding on the live connection-count path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Don't rebind pairing listener for invalid port input

Typing an out-of-range port (e.g. 70000) persists the raw value and
syncToSettings mapped it to the default via configuredPort(), so a
listener running on a custom valid port would restart and move to the
default (dropping devices) while the UI only showed an "invalid" warning.

Add resolvedDesiredPort() which returns nil for an out-of-range stored
value; syncToSettings then reuses the applied port (no restart) until a
valid port is entered. A fresh start still binds the default.

Addresses autoreview finding on invalid-port handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Source display-name placeholder from static system name, not live status

The status stream only fires on listener/route/connection events, so the
snapshot's displayName went stale when the user edited or cleared the
override (the display-name write is a plain UserDefaults change that posts
no status notification). Drop displayName from the snapshot and add
SettingsHostActions.mobilePairingDefaultDisplayName() returning the Mac's
system name (Host.current().localizedName), which the placeholder uses.
That name is stable, so the placeholder never goes stale. The override
itself still drives the real pairing name via MobileHostIdentity.

Addresses autoreview finding on the display-name placeholder. (The curated
search-entry title finding is the existing package convention — all 103
entries hard-code English titles; the row labels and search synonyms are
localized, so this is left consistent with the rest of the catalog.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Doc the SettingsHostActions mobile default implementations

Add Swift-DocC one-liners to the new mobilePairingStatus/
mobilePairingStatusUpdates/mobilePairingDefaultDisplayName default
implementations to satisfy the package documentation policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add explicit Apply button with port availability check

Editing the port no longer rebinds the listener. The field is a local
draft; an Apply button (enabled when the draft is valid and differs from
the port in effect) checks the port is free before doing anything:

- free  -> persist + rebind (devices reconnect on the new port)
- in use -> leave the running listener untouched, show "Port X is in use,
  still listening on Y"
- pairing off -> save for when pairing is enabled

Availability is a synchronous one-shot bind probe (INADDR_ANY, no
SO_REUSEADDR, matching NWListener's default); the live bound-port status
stays authoritative so any rare dual-stack disagreement self-corrects.
Decision logic is the pure, unit-tested portApplyDecision.

Also fix a latent gap: a preferred port held by another process can
surface as .waiting(.posix(.EADDRINUSE)) rather than .failed, where the
listener would wait forever; treat address-unavailable .waiting the same
as a failure so the ephemeral fallback fires. Shared via
handleListenerBindFailure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix port draft sync + IPv6-accurate availability probe (autoreview)

Two autoreview findings on the Apply flow:

- P1: the field seeded its draft from DefaultsValueModel's initial *default*
  (it yields the saved value asynchronously), so a user with a saved port
  saw the default and could overwrite it. The field now tracks port.current
  via an optional editedPort (nil = follow persisted, set = user edit), so it
  reflects the saved port once loaded and never clobbers it.

- P2: the IPv4-only bind probe missed an IPv6-only conflict, so apply could
  restart and drop connections despite the "untouched on conflict" contract.
  Probe with a throwaway NWListener using the same NWParameters as the real
  bind (dual-stack), one-shot continuation under the lock carve-out.
  applyConfiguredPort is now async and probes only when a running listener
  would move to a different in-range port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Force rebind when applying a freed port after ephemeral fallback

After an ephemeral fallback, appliedPreferredPort holds the configured
port while the listener is on an ephemeral one. Re-applying the (now-freed)
configured port persisted the same value, so the settings observer's
syncToSettings saw no change and the listener stayed on the ephemeral port
even though apply reported success. applyConfiguredPort now restarts
directly whenever the listener is not bound to the requested port, instead
of relying on a persisted-value change to drive the rebind.

Addresses autoreview finding on the apply state machine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Don't show stale Apply feedback after the pairing toggle changes

The Apply message was cleared only on a port edit, so toggling pairing
could leave a contradictory note: "Will use port X when iOS Pairing is on"
after enabling, or "Still listening on Y" after disabling. Gate the
saved-for-later note to pairing-off and the in-use note to pairing-on, so
the live indicator takes over the moment the toggle flips (these read the
@Observable toggle state, so they re-evaluate reactively).

Addresses autoreview finding on stale Apply feedback. (The curated
search-title localization finding is the catalog's documented English-only
design — "English-only until the package ships an xcstrings catalog" — so
localizing only the new entries would contradict it and split the table;
the row labels and synonyms are localized.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address CodeRabbit: IPv6 endpoint brackets, accurate fallback flag, honest defaults

- MobilePairingRoute.endpoint wraps IPv6 literals in brackets ([host]:port)
  per RFC 3986 so the port colon isn't ambiguous.
- makeStatus reports usesEphemeralFallback from the stored bind outcome
  (listenerUsesEphemeralFallback) instead of recomputing listenerPort vs the
  current configured port, which could flip during an edit/restart window.
- syncToSettings() / applyConfiguredPort() drop their UserDefaults parameter:
  they drive the live singleton listener, which always binds against
  UserDefaults.standard (start/restart read it too), so a caller-supplied
  store was never honored for the actual bind. The pure read-only statics keep
  their defaults parameter for unit testing.

CodeRabbit's in-field port-validation nitpick is already handled: an
out-of-range value disables Apply and shows the range warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix port-availability probe hang: NWListener needs a newConnectionHandler

NWListener does not transition to .ready unless newConnectionHandler is set
before start(), so the probe never resumed its continuation on a *free*
port — hanging applyConfiguredPort (and the Apply button) on the common
success path. Set a reject-everything newConnectionHandler on the probe.

Caught by Greptile (P1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Harden port probe against hangs + localize new search titles

- isPortAvailable now races the NWListener probe against a bounded 2s
  deadline via a task group; cancellation tears down the probe listener
  through a cancellation handler (.cancelled resolves as unavailable), so an
  unclassified/stuck listener state can never hang Apply. On timeout the port
  is reported unavailable (safe: leaves the running listener untouched).
- Curated search-entry titles for the new mobile rows are now localized
  (reuse the row-label keys), so JP search results don't show English.

Addresses autoreview (P2 hang, P3 localization) and Cursor/Greptile probe
findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make Apply atomic: make-before-break instead of probe-then-restart

The probe-then-restart path was not atomic: the requested port could be
taken (or the probe's own socket not yet released) between the availability
probe and the real bind, by which point the old listener had already been
stopped — dropping connections and landing on an ephemeral port despite the
"in-use port leaves the running listener untouched" contract.

applyConfiguredPort now binds a *candidate* listener on the requested port
while the current one keeps running, and only tears down the old listener
and adopts the candidate once it actually reaches .ready. If the candidate
can't bind (in use), it's discarded and the live listener is untouched
(portInUse). A bounded, cancellable 2s deadline guarantees Apply can't
hang. The separate availability probe is removed; the candidate bind is the
real bind. portApplyDecision becomes the pure pre-bind classifier
portApplyPreBindOutcome (nil = a real bind is needed).

Addresses autoreview P1 (non-atomic apply).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document OSC background routing

* Add Mobile Connect to the command palette (#5518)

* Add Mobile Connect to the command palette

New "Mobile Connect" entry in the Cmd+Shift+P command palette that opens
the iOS/iPadOS pairing window (same shared MobilePairingWindowController
path as Settings → Mobile → Pair a Device). Searchable by ios, ipados,
iphone, ipad, pair, pairing, mobile, connect, device, phone, tablet, qr.

Keywords live in one place (ContentView.commandPaletteMobileConnectKeywords)
so the contribution and its behavioral test can't drift. Test runs the real
fuzzy search engine and asserts the command is the top result for "ios" and
"ipados" (plus iphone/ipad/pair/mobile connect) against a dense decoy corpus.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Rename palette command to "Connect iPhone/iPad" + localize all languages

Verb-first name matching the palette's house style; the visible label is
"Connect iPhone/iPad" while keywords still match mobile, phone, ios, ipados,
iphone, ipad, pair, connect, device, tablet, qr. Title and subtitle are now
translated for all 20 locales in Localizable.xcstrings (was en + ja).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Localize numbered shortcut digit validation

* fix: measure visible sidebar rows

* Add auth commands to command palette (#5529)

* Bump version to 0.64.14

* Pair onboarding: drop leading row icons, keep text (#5520)

* Pair onboarding: drop leading row icons, keep text

The "Pair your iPhone" requirements checklist showed a leading SF Symbol
per row (person/checkmark for sign-in, globe/checkmark/warning for
Tailscale). Remove the glyph so each row is just title + subtitle text;
the "Get Tailscale" trailing link and all state-driven subtitles stay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pair onboarding: also drop header computer/iPhone icon

"Get rid of icons, just leave the text" covers the whole card. Removing
the header glyph too makes the heading, sign-in row, and Tailscale row
all text-only and flush-left at the same inset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix React script warning in web layout (#5525)

* fix: avoid React script warning in web layout

* fix: preserve theme bootstrap behavior

* fix: keep initial theme color media-safe

* fix: insert theme bootstrap outside hydration

* fix: share theme color constants

* CodeRabbit: stop blocking merges (request_changes_workflow=false) (#5538)

CodeRabbit was submitting reviews in the Request Changes state, which
shows as a blocker in the PR merge box. It is not a required status
check, so flipping request_changes_workflow to false makes it post the
same findings as plain Comment reviews that never block a merge.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Match iOS viewport border to pane divider color (#5530)

* Match iOS viewport border to pane divider color

When an iOS device is connected, macOS draws a border marking the area
visible to the phone. It hardcoded NSColor.separatorColor at 0.95 alpha,
which renders as a bright near-white line in dark mode and doesn't match
any other border in the app.

Stroke the resolved split-divider color instead (the single source of
truth pane dividers already use via GhosttyConfig.resolvedSplitDividerColor),
so the viewport border matches every other border and the color lives in
one place. Repaint the overlay on background changes so it tracks theme
switches while connected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Match iOS viewport border to window-chrome separator color

Ground-truth pixel sampling of the rendered borders showed the
viewport border (right/bottom of the visible area) did not match the
pane outline, sidebar trailing edge, and tab-bar separators: those use
WindowChromeSeparatorColor (~rgb(54,55,49) over the default dark bg),
while the split-divider color is darker and the old separatorColor@0.95
was much brighter (~rgb(74,76,71)).

Stroke WindowChromeSeparatorColor.current() so the viewport border is
pixel-identical to every other chrome border, using the same single
source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* iOS: redesign the default terminal toolbar layout (#5532)

Reorder the iOS terminal accessory bar so the high-traffic keys sit up
front: after the modifier keys come Tab, ^C/^D, the Claude/Codex
launchers, the arrow keys, then a Clear button (^L, relabeled "Clear").
The zoom controls move from the leading pinned region to a new trailing
pinned region at the end of the bar. The remaining punctuation and
navigation keys keep their slots, with the pipe positioned after @.

The curated default arrangement lives in
TerminalInputAccessoryAction.defaultConfigurableOrder, separate from the
enum rawValue order, so persisted display-order identifiers are
untouched. TerminalAccessoryLayoutReducer takes the default order and
defensively appends any omitted configurable id, so a gap in the curated
list can never drop an action from the bar.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* iOS: rename and pin workspaces from the phone (#5512)

* iOS: rename and pin workspaces from the phone

Adds a per-workspace context menu (Rename, Pin/Unpin) to the iOS workspace
list, sorts pinned workspaces to the top with a pin glyph, and a rename
sheet. The phone drives the Mac's existing workspace-scoped `workspace.action`
RPC (pin/unpin/rename).

Security: `MobileHostService` only newly authorizes `workspace.action`, and
only its pin/unpin/rename sub-actions. The action param is normalized exactly
as the handler's `v2ActionKey` (lowercase, '-'->'_') so the gate and handler
can never disagree on which action runs, and workspace scope is enforced with
the same check used for terminal input: a workspace-scoped ticket may only act
on its own workspace, a Mac-wide pairing on any, a terminal-scoped ticket on
none. The destructive/global sub-actions (move_*, close_*, set_color, …) and
the global methods (reorder_many, group.*, the dedicated workspace.rename) stay
Mac-only. New XCTest cases in MobileHostAuthorizationTests lock this down.

The Mac now emits `is_pinned` in the mobile workspace-list payload, and the
workspace-list observer watches `$isPinned` (and hashes it) so a pure pin
toggle pushes to the phone. iOS decodes it backward-compatibly (nil on older
Macs), updates the list optimistically with rollback on RPC failure, and the
authoritative `workspace.updated` push reconciles. en+ja localized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix PR2 mobile gate: route workspace.action through the real mobile dispatch

The live mobile data plane authorizes identity via same-Stack-account
verification and gates method exposure with an explicit allowlist in
TerminalController.mobileHostHandleRPC (default -> method_not_found), not via
MobileHostService.ticketAuthorizationError (which is only reached by the test
hook). The earlier allowlist edit + tests targeted that test-only function, so
workspace.action would have returned method_not_found at runtime and rename/pin
would silently fail.

Revert the ineffective MobileHostService change and its tests. Add a gated
case "workspace.action" to mobileHostHandleRPC via v2MobileWorkspaceAction,
which rejects every sub-action except pin/unpin/rename (normalized exactly as
v2ActionKey) before calling v2WorkspaceAction, so move_*/close_*/set_color/etc.
stay Mac-only. Cover the gate with a pure mobileAllowsWorkspaceAction test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR2 review: roll back only the targeted workspace field, match-guarded

Autoreview P1: the optimistic rename/pin restored the whole `workspaces`
snapshot on RPC failure, which could clobber newer authoritative state (a
reconnect or a workspace.updated refresh landing while the request was in
flight). Roll back only the single workspace's name/isPinned, and only when our
optimistic value is still present, so a concurrent refresh is never reverted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR2 review: drop optimistic rename/pin, rely on authoritative push

Second review iteration still found an optimistic-rollback race (overlapping
pin then unpin, both failing, could leave stale local state). Stop patching the
rollback and remove the optimistic mutation entirely: the Mac applies the
rename/pin and its workspace-list observer pushes workspace.updated (now also on
$isPinned), which refreshes the list. Fire-and-forget RPC, no local mutation, so
no rollback and no overlap race. (The review's "removes terminal OSC/background
handling" note is incorrect; this branch touches no terminal-rendering code.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR2 review: gate rename/pin UI on a host capability

Review P2: the rename/pin affordances were shown on every connected Mac, but an
older Mac lacking the new mobile workspace.action handler returns
method_not_found, so the actions silently no-op. Advertise a workspace.actions.v1
capability in mobile.host.status, capture it on the client when reading host
status, and only pass the rename/pin closures when the connected Mac supports it
(reusing the existing nil-closure hiding). Reset on disconnect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR2: build the capability-gated closures as literals

The previous commit's `store.supportsWorkspaceActions ? method : nil` ternary
tripped a Swift type-checker bug ("failed to produce diagnostic") inside the
large WorkspaceListView initializer. Build the optional rename/pin closures as
explicit closure literals capturing the store instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR2 review: advertise workspace.actions.v1 on the live status paths

Review P1: the mobile listener intercepts mobile.host.status and returns the
public-status cache / publicHostStatusResult before TerminalController runs, so
adding workspace.actions.v1 only to TerminalController's status left it invisible
to the iOS client. supportsWorkspaceActions stayed false and rename/pin were
hidden even on a supporting Mac.

Consolidate all three capability lists into one source of truth
(MobileHostService.mobileHostCapabilities), advertised on every status path, so
the lists cannot drift again. Add a contract test asserting the shared list
advertises workspace.actions.v1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR2 review: require an explicit valid workspace_id for mobile actions

Review P2: v2MobileWorkspaceAction forwarded to v2WorkspaceAction, which falls
back to the Mac's selected workspace when workspace_id is missing. A malformed or
omitted workspace_id from the mobile plane could therefore pin/rename the wrong
workspace. Validate like the other mobile handlers and additionally require the
id to be present and resolvable before dispatching this mutating action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* iOS: multi-Mac host switcher (#5513)

* iOS: multi-Mac host switcher

Adds a Settings -> Connection -> "Switch Mac" picker that lists every Mac
paired with this device, marks the active one, switches the live connection on
tap, forgets on swipe, and pairs another Mac by scanning its QR without
dropping the others.

The on-device SQLite store already persisted N paired Macs; only the UI was
missing (loadAll was test-only). MobileShellComposite gains pairedMacs,
activeMacDeviceID, loadPairedMacs(), switchToMac(macDeviceID:) (setActive then
reconnect via the existing launch-time reconnect path), and
forgetMac(macDeviceID:). MobileHostPickerView drives them, reached from
MobileSettingsView; the store is threaded as an optional through
WorkspaceShellView -> WorkspaceListView -> MobileSettingsView so workspace rows
stay value-only.

Scope: switches among distinct Macs (each QR pairing stores a real
macDeviceID). Multiple cmux instances on one Mac share a macDeviceID and need a
schema change to distinguish, so that remains a deliberate follow-up. en+ja
localized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR3 review: scope setActive's clear to the target Mac's Stack user

Autoreview P1: switchToMac called the unscoped setActive, which cleared
is_active across every row. On a shared device, switching hosts for one signed-in
user wiped another user's active pairing, so they failed to auto-reconnect after
signing back in. Scope the clear to the target Mac's own stack_user_id via a
null-safe subquery (mirroring upsert's scoped clear). Add a store regression
test proving a second Stack user's active pairing survives the switch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR3 review: connect before persisting active Mac; clear cache on sign-out

Two review findings:
- P1: switchToMac persisted the new active row before the reconnect, so
  switching to an offline/stale-route Mac stranded the user on an unreachable
  host that recovery kept retrying, with no way back to the switcher. Now it
  connects to the target's route first and persists setActive only on a
  successful connect, so a failed switch leaves the previously-working Mac
  active and reachable.
- P2: the cached pairedMacs list could leak across signed-in users on a shared
  device. Clear it on sign-out and gate loadPairedMacs on isSignedIn.

Also drop the unreliable activeMacDeviceID (the live attach ticket carries a
transient manual id after a reconnect, not the stored Mac's real id); the
switcher now marks the active row by the store's isActive flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR3 review: fix switch race, cross-user load, and active-row forget

Third review pass, three findings:
- P1: a switch superseded mid-connect could persist the wrong active Mac
  (post-hoc connectionState check wasn't tied to this connect). Persist setActive
  only when the live route matches this Mac's normalized host:port.
- P1: loadPairedMacs passed a nil Stack user id straight to loadAll, which
  returns every user's pairings. Treat a missing current user as no pairings.
- P2: forgetting the active row deleted by the live ticket's transient manual id,
  which may not be the stored row, leaving it behind. Always remove the selected
  real id, and tear down the live connection via the new disconnectLiveConnection
  helper when that row is active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR3 review: restore previous connection on failed switch; guard stale load

Fourth review pass:
- P1: connectManualHost is destructive (replaces the live client before the new
  route proves usable), so a failed switch to an offline/stale Mac dropped the
  working session. Capture the previously-active Mac and, when the switch does
  not connect, reconnect to it (it remains the store's active row since setActive
  only runs on success), so a failed switch self-restores instead of stranding.
- P2: loadPairedMacs assigned results after an await without rechecking the user;
  a slow load could repopulate another user's hosts after sign-out. Re-check
  isSignedIn and the current Stack user after the await and discard on mismatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* PR3: document disconnectAndForgetActiveMac (Aziz doc policy)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add React and Solid agent session panels (#4429)

* Escape inlined agent session bundles

* Use file URL for agent session shells

* Avoid stale agent session web cache

* Use persistent agent session web store

* Add CLI path for agent session surfaces

* Avoid focus reads during PR refresh scheduling

* Load agent session shell from HTML string

* Fill agent session web panels

* Fix agent session web view hosting

* Flush agent session page paint after load

* Flush agent session page after render frames

* Flush agent session paint when visible

* Address agent session review findings

* Polish transparent agent session UI

* Make agent session panel background transparent

* Speak Codex app-server JSON-RPC

* Avoid blanking retained agent webviews

* Auto-start themed agent sessions

* Cover agent GUI auto-start po…

This branch was successfully deployed

1 active deployment
Preview – cmux — 4ba99e59 Deployed May 13, 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.

Menubar global cross-window full-text search (local, sub-100ms)

2 participants