Menubar global search P1 - #3908
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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. ChangesGlobal Search Feature
Sequence DiagramsequenceDiagram
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:)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (3 errors, 1 warning)
✅ Passed checks (11 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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
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 SummaryAdds a menubar-anchored global search palette backed by a local SQLite/FTS5 index (
Confidence Score: 4/5Mostly 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: Sources/Search/MenubarSearchPopover.swift — the Important Files Changed
Sequence DiagramsequenceDiagram
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(_:)
|
| 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) | ||
|
|
There was a problem hiding this comment.
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!
| 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") | ||
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
📒 Files selected for processing (14)
GhosttyTabs.xcodeproj/project.pbxprojResources/Localizable.xcstringsSources/App/MenuBarExtraController.swiftSources/AppDelegate.swiftSources/KeyboardShortcutSettings.swiftSources/Panels/BrowserPanel.swiftSources/Panels/MarkdownPanel.swiftSources/Search/GlobalSearchCoordinator.swiftSources/Search/MenubarSearchPopover.swiftSources/Search/SearchIndex.swiftcmuxTests/GlobalSearchShortcutSettingsTests.swiftcmuxTests/SearchIndexTests.swiftweb/data/cmux-shortcuts.tsweb/data/cmux.schema.json
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
GhosttyTabs.xcodeproj/project.pbxprojSources/AppDelegate.swift
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
GhosttyTabs.xcodeproj/project.pbxprojResources/Localizable.xcstringsSources/App/MenuBarExtraController.swiftSources/AppDelegate.swiftSources/Search/GlobalSearchCoordinator.swiftSources/Search/MenubarSearchPopover.swiftSources/Search/SearchIndex.swiftcmuxTests/SearchIndexTests.swift
…obal-search # Conflicts: # Sources/Panels/MarkdownPanel.swift
| default: | ||
| if flags.contains(.command), | ||
| !flags.contains(.option), | ||
| !flags.contains(.control) { | ||
| return !isTextEditingCommand(event) | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
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.
| default: | |
| if flags.contains(.command), | |
| !flags.contains(.option), | |
| !flags.contains(.control) { | |
| return !isTextEditingCommand(event) | |
| } | |
| return false | |
| } | |
| default: | |
| return false |
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.
| 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 |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 5c9ff7e. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ 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 |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 4ba99e5. Configure here.
| "en": { "stringUnit": { "state": "translated", "value": "Type to search" } }, | ||
| "ja": { "stringUnit": { "state": "translated", "value": "入力して検索" } } | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 4ba99e5. Configure here.
* 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>
* 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>
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>
|
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. |
…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…


Summary
Scope
Validation
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
SearchIndexplusGlobalSearchCoordinator/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
globalSearchhotkey (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
globalSearchshortcut (⌥⌘F), reliable activation, and refined capture ownership to prevent stale or duplicate work.New Features
SearchIndex) with upsert/purge/search and BM25 ranking/snippets; starts at app launch and is skipped under XCTest.GlobalSearchCoordinator+GlobalSearchPanelCaptureManager) that track window/workspace/panel; debounced captures for browser (on navigation) and markdown (on file updates); purge on close/missing.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.globalSearchviaKeyboardShortcutSettingsandSystemWideHotkeyController; schema/docs and localized labels updated.Bug Fixes
globalSearchisn’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
Improvements
Localization
Tests