Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
ba5e7ae
Add Codex transcript fallback regression
Jun 26, 2026
731d7a5
Bind Codex transcript fallback to exact session id
Jun 26, 2026
1bd0bc2
Share Codex transcript identity matching
Jun 26, 2026
24bb84a
Fail closed on Codex rollouts without session metadata
Jun 26, 2026
951053f
Export Foundation for Codex matcher API
Jun 27, 2026
5580b7c
Bound Codex transcript lookup
Jun 27, 2026
bacff62
Guard iOS glass APIs for older SDKs
Jun 27, 2026
5592081
Keep mobile composer within line budget
Jun 27, 2026
beb8420
Isolate chat transcript row hosting on main actor
Jun 27, 2026
94fb14b
Use macos-26 fallback for iOS CI
Jun 27, 2026
8484eef
Use depot fallback for iOS CI
Jun 27, 2026
31170f0
Fix iOS localized diff interpolation
Jun 27, 2026
d07f317
Resolve Codex transcripts off main actor
Jun 27, 2026
d77049a
Pin iOS workflow fallback runner
Jun 27, 2026
db0f66b
Expose transcript resolution failures to service extension
Jun 27, 2026
6a8320c
Clear stale Codex resolution failures
Jun 27, 2026
9d27b3e
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
Jun 27, 2026
64579dd
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
Jun 27, 2026
ec6df78
Handle ended Codex transcript resolution
Jun 27, 2026
10e4873
Reattach chat scroll edge interaction
Jun 27, 2026
537e314
Sort Codex transcript candidates once
Jun 27, 2026
86b1a4d
Use hosted macOS 26 for iOS CI
Jun 27, 2026
6a0cd98
Route iOS CI through runner variable
Jun 27, 2026
7a5f11c
Fix iOS keyboard dismiss cleanup isolation
Jun 27, 2026
61ee95d
Isolate mobile composer view helpers
Jun 27, 2026
38e77b8
Keep composer annotations within length budget
Jun 27, 2026
509b07c
Build composer picker label on main actor
Jun 27, 2026
ab4dce5
Isolate terminal key command creation
Jun 27, 2026
185e022
Fix mobile terminal surface isolation
Jun 27, 2026
4aea89d
Avoid isolated deinit in mobile shell
Jun 27, 2026
66112d6
Avoid sending notification settings across actors
Jun 27, 2026
cc8c8b6
Merge origin/main into issue 5550
Jun 29, 2026
034d27d
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jun 30, 2026
6407663
Restore isolated deinit for mobile shell teardown
austinywang Jun 30, 2026
09bfa9b
Make Codex resolver tests robust to /var symlink canonicalization
austinywang Jun 30, 2026
00ce0a4
Fix temp-home symlink resolution in Codex resolver tests
austinywang Jun 30, 2026
3421dbe
Compare Codex resolver test paths by realpath, not raw string
austinywang Jun 30, 2026
72c484b
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jun 30, 2026
7d93d26
Route remaining raw Ghostty surface uses through the Sendable handle
austinywang Jun 30, 2026
5a3d565
Stop tailer file watchers in the Codex ended-session test
austinywang Jun 30, 2026
3b7b621
Don't let a stale Codex fallback scan overwrite a recorded path
austinywang Jun 30, 2026
d57251a
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jun 30, 2026
461ed0c
Merge origin/main into issue-5550-token-count-shows-climbing-phantom-…
austinywang Jul 2, 2026
9120c05
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jul 2, 2026
d2276a9
Add failing regression test for Codex recorded-path overwrite
austinywang Jul 2, 2026
182b812
Wire Codex transcript resolver regression as a non-tolerant CI gate
austinywang Jul 2, 2026
880c538
Guard the shared Codex resolution path against clobbering a recorded …
austinywang Jul 2, 2026
753de4a
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jul 2, 2026
992afa7
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jul 2, 2026
e269044
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jul 3, 2026
0f1c066
Address PR closeout policy findings
austinywang Jul 3, 2026
c578136
Merge remote-tracking branch 'origin/main' into issue-5550-token-coun…
austinywang Jul 4, 2026
31ce781
Fix duplicate test source wiring
austinywang Jul 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
34499 CLI/cmux.swift
34428 CLI/cmux.swift
17954 Sources/AppDelegate.swift
16427 Sources/ContentView.swift
14270 Sources/TerminalController.swift
Expand All @@ -12,7 +12,7 @@
9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8032 Sources/Panels/BrowserPanelView.swift
8016 CLI/cmux_open.swift
7743 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
7761 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
7405 cmuxTests/WorkspaceUnitTests.swift
7218 cmuxTests/WorkspaceRemoteConnectionTests.swift
6359 cmuxTests/SessionPersistenceTests.swift
Expand All @@ -24,8 +24,8 @@
4483 Sources/cmuxApp.swift
4482 Sources/Panels/FilePreviewPanel.swift
4367 cmuxTests/BrowserPanelTests.swift
4283 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
4121 Sources/BrowserWindowPortal.swift
3964 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
Expand Down Expand Up @@ -58,7 +58,7 @@
1656 Sources/FileExplorerView.swift
1652 cmuxTests/CMUXCLIErrorOutputRegressionTests.swift
1649 cmuxTests/TerminalControllerSocketSecurityTests.swift
1604 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1603 Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
1560 cmuxTests/TextBoxMentionCompletionTests.swift
1500 cmuxUITests/MultiWindowNotificationsUITests.swift
1499 cmuxTests/OmnibarAndToolsTests.swift
Expand Down
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,29 @@ jobs:
-only-testing:cmuxTests/GhosttyOptionAsAltModsTests \
test

- name: Run Codex transcript fallback session-binding regression
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# Focused gate for the Codex transcript fallback race
# (https://github.com/manaflow-ai/cmux/issues/5550). The full "Run unit
# tests" step below tolerates failures by parsing only the XCTest
# "Executed ... (0 unexpected)" summary, which does NOT count Swift
# Testing (@Test/#expect) issues. This suite is Swift Testing, so a
# regression here would otherwise be silently swallowed. Keep this
# targeted only-testing invocation as a non-tolerant guard.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
scripts/ci/run-in-console-session.sh \
scripts/ci/run-app-host-xcodebuild.sh \
-project cmux.xcodeproj -scheme cmux-unit -configuration Debug \
-derivedDataPath "$CMUX_DERIVED_DATA_PATH" \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/AgentChatTranscriptResolverTests \
test

- name: Run unit tests
run: |
set -euo pipefail
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/test-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,8 @@ jobs:
ios-simulator:
needs: detect-ios-changes
if: ${{ needs.detect-ios-changes.outputs.should_run == 'true' }}
# Blacksmith, not the bare `macos-26` label (which our self-hosted fleet
# carries). iOS simulator XCTest runs inside the Simulator, not as a
# foregrounded Mac app, so the Blacksmith macOS foreground limitation that
# blocks macOS app-host XCTest does not apply here; this lane is verified
# green on blacksmith-6vcpu-macos-26.
# Keep iOS CI runner selection behind the repo variable so overflow can
# switch between paid macOS runner providers without a workflow edit.
runs-on: ${{ vars.MACOS_RUNNER_IOS || 'blacksmith-6vcpu-macos-26' }}
timeout-minutes: 35
strategy:
Expand Down
73 changes: 1 addition & 72 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25304,78 +25304,7 @@ struct CMUXCLI {
}

func findCodexTranscriptPath(sessionId: String, env: [String: String]) -> String? {
let normalizedSessionId = sessionId.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalizedSessionId.isEmpty else { return nil }

let codexHome = normalizedHookValue(env["CODEX_HOME"]) ?? "~/.codex"
let sessionsURL = URL(fileURLWithPath: NSString(string: codexHome).expandingTildeInPath, isDirectory: true)
.appendingPathComponent("sessions", isDirectory: true)
let fileManager = FileManager.default
var newest: URL?
var newestModificationDate: Date?
for directoryURL in recentCodexSessionDirectories(sessionsURL: sessionsURL, fileManager: fileManager) {
guard let urls = try? fileManager.contentsOfDirectory(
at: directoryURL,
includingPropertiesForKeys: [.contentModificationDateKey],
options: [.skipsHiddenFiles]
) else {
continue
}

for url in urls {
guard url.pathExtension == "jsonl",
url.lastPathComponent.contains(normalizedSessionId) else {
continue
}
let modificationDate = (try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
if newest == nil || modificationDate > (newestModificationDate ?? .distantPast) {
newest = url
newestModificationDate = modificationDate
}
}
}
return newest?.path
}

private func recentCodexSessionDirectories(sessionsURL: URL, fileManager: FileManager) -> [URL] {
var directories: [URL] = []
var seenPaths = Set<String>()

func appendIfDirectory(_ url: URL) {
guard seenPaths.insert(url.path).inserted else { return }
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory),
isDirectory.boolValue else {
return
}
directories.append(url)
}

appendIfDirectory(sessionsURL)

var utcCalendar = Calendar(identifier: .gregorian)
utcCalendar.timeZone = TimeZone(secondsFromGMT: 0) ?? .current
for calendar in [Calendar.current, utcCalendar] {
for dayOffset in -14...1 {
guard let date = calendar.date(byAdding: .day, value: dayOffset, to: Date()) else {
continue
}
let components = calendar.dateComponents([.year, .month, .day], from: date)
guard let year = components.year,
let month = components.month,
let day = components.day else {
continue
}
appendIfDirectory(
sessionsURL
.appendingPathComponent(String(format: "%04d", year), isDirectory: true)
.appendingPathComponent(String(format: "%02d", month), isDirectory: true)
.appendingPathComponent(String(format: "%02d", day), isDirectory: true)
)
}
}

return directories
nil
}

private static let codexMonitorLeaseDirectoryName = "codex-monitor-leases"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import CmuxAgentChat
import CmuxMobileSupport
import Foundation
import SwiftUI

#if os(iOS)
import PhotosUI
import UIKit
#endif

public struct ChatComposerView: View {
private let agentState: ChatAgentState
private let agentKind: ChatAgentKind
Expand Down Expand Up @@ -39,7 +37,6 @@ public struct ChatComposerView: View {
private static let maxAttachmentDimension: CGFloat = 2048
private static let jpegQuality: CGFloat = 0.85
private static let hardStopWindow: TimeInterval = 2

public init(
agentState: ChatAgentState,
agentKind: ChatAgentKind,
Expand Down Expand Up @@ -97,19 +94,23 @@ public struct ChatComposerView: View {
}

#if os(iOS)
@ViewBuilder
@MainActor @ViewBuilder
private var composerSurface: some View {
#if compiler(>=6.2)
if #available(iOS 26.0, *) {
GlassEffectContainer {
composerStack
}
} else {
composerStack
}
#else
composerStack
#endif
}
#endif

private var composerStack: some View {
@MainActor private var composerStack: some View {
VStack(spacing: 8) {
if isEnded {
endedRow
Expand Down Expand Up @@ -166,7 +167,7 @@ public struct ChatComposerView: View {

// MARK: - Field row

private var fieldRow: some View {
@MainActor private var fieldRow: some View {
HStack(alignment: .bottom, spacing: 8) {
#if os(iOS)
attachButton
Expand Down Expand Up @@ -225,7 +226,7 @@ public struct ChatComposerView: View {
agentState == .ended
}

private var endedRow: some View {
@MainActor private var endedRow: some View {
HStack(spacing: 12) {
Text(
String(
Expand Down Expand Up @@ -254,7 +255,7 @@ public struct ChatComposerView: View {

// MARK: - Send / stop button

@ViewBuilder
@MainActor @ViewBuilder
private var sendButton: some View {
if hasContent {
Button(action: performSend) {
Expand Down Expand Up @@ -375,14 +376,13 @@ public struct ChatComposerView: View {
isDraftFocused = true
}

private var attachButton: some View {
PhotosPicker(selection: $pickedItems, maxSelectionCount: 4, matching: .images) {
MobileComposerIconLabel(
systemImage: "paperclip",
foregroundStyle: AnyShapeStyle(Color.secondary.opacity(0.8)),
size: controlHeight
)
}
@MainActor private var attachButton: some View {
let label = MobileComposerIconLabel(
systemImage: "paperclip",
foregroundStyle: AnyShapeStyle(Color.secondary.opacity(0.8)),
size: controlHeight
)
return PhotosPicker(selection: $pickedItems, maxSelectionCount: 4, matching: .images) { label }
.buttonStyle(.plain)
.accessibilityIdentifier("ChatComposerAttach")
.accessibilityLabel(
Expand All @@ -398,7 +398,7 @@ public struct ChatComposerView: View {
}
}

private var micButton: some View {
@MainActor private var micButton: some View {
let listening = dictation.state.isListening
return MobileComposerIconButton(
systemImage: "mic",
Expand All @@ -423,7 +423,7 @@ public struct ChatComposerView: View {
}
}

private var attachmentStrip: some View {
@MainActor private var attachmentStrip: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(Array(attachments.enumerated()), id: \.element.id) { index, attachment in
Expand All @@ -449,7 +449,7 @@ public struct ChatComposerView: View {
}
}

private func removeButton(id: String, index: Int) -> some View {
@MainActor private func removeButton(id: String, index: Int) -> some View {
Button {
removeAttachment(id: id)
} label: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,20 @@ struct KeyboardDismissTap: UIViewRepresentable {
// and dismisses the keyboard on every tap across other screens
// that share the window.
if window !== installedWindow {
installedWindow?.removeGestureRecognizer(recognizer)
installedWindow = nil
uninstallRecognizer()
}
guard let window else { return }
guard installedWindow == nil, let window else { return }
window.addGestureRecognizer(recognizer)
installedWindow = window
}

deinit {
uninstallRecognizer()
}

func uninstallRecognizer() {
installedWindow?.removeGestureRecognizer(recognizer)
installedWindow = nil
}

@objc private func handleTap() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,14 @@ public struct ChatFileEditCardView: View {

private func diffLineAccessibilityLabel(_ line: String) -> String {
if line.hasPrefix("+") {
let addedLine = String(line.dropFirst())
return String(localized: "chat.diff.added.accessibility",
defaultValue: "Added: \(line.dropFirst())", bundle: .module)
defaultValue: "Added: \(addedLine)", bundle: .module)
}
if line.hasPrefix("-") {
let removedLine = String(line.dropFirst())
return String(localized: "chat.diff.removed.accessibility",
defaultValue: "Removed: \(line.dropFirst())", bundle: .module)
defaultValue: "Removed: \(removedLine)", bundle: .module)
}
if line.hasPrefix("@@") {
return String(localized: "chat.diff.hunk.accessibility",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,25 @@ public final class MobileShellComposite: MobileTerminalOutputSinking {
self.pairingAttemptID = UUID()
}

// This non-UIView @MainActor store can have its last reference released off
// the main actor (a background continuation/callback/detached task may hold
// it), so teardown must HOP to the main actor via `isolated deinit`, not
// assert it — `MainActor.assumeIsolated` would trap on an off-main release.
// cmux ships on Swift 6.2 (the isolated-deinit path); the #else only keeps
// the older compat toolchain, which lacks `isolated deinit`, compiling.
#if compiler(>=6.2)
isolated deinit {
cancelInFlightWorkForTeardown()
}
#else
deinit {
MainActor.assumeIsolated {
cancelInFlightWorkForTeardown()
}
}
#endif

private func cancelInFlightWorkForTeardown() {
presenceTask?.cancel()
networkPathObservationTask?.cancel()
terminalEventListenerTask?.cancel()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ public final class MobilePushCoordinator {
/// and persist the flag. Returns whether authorization was granted.
@discardableResult
public func enable() async -> Bool {
let priorStatus = await UNUserNotificationCenter.current()
.notificationSettings().authorizationStatus
let priorStatus = await currentNotificationAuthorizationStatus()
// Only an undetermined status produces a real OS prompt; gate the
// "shown" event on it so a re-toggle of an already-decided status does
// not log a phantom prompt.
Expand Down Expand Up @@ -172,6 +171,15 @@ public final class MobilePushCoordinator {
UIApplication.shared.unregisterForRemoteNotifications()
}

private func currentNotificationAuthorizationStatus() async -> UNAuthorizationStatus {
let rawValue = await withCheckedContinuation { continuation in
UNUserNotificationCenter.current().getNotificationSettings { settings in
continuation.resume(returning: settings.authorizationStatus.rawValue)
}
}
return UNAuthorizationStatus(rawValue: rawValue) ?? .notDetermined
}

/// Hand a freshly-registered APNs token to the network layer.
public func handleDeviceToken(_ token: Data) async {
await registration.register(deviceToken: token)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct SignInView: View {

@ViewBuilder
private var signInEntrySwitcher: some View {
#if os(iOS)
#if os(iOS) && compiler(>=6.2)
if #available(iOS 26.0, *) {
GlassEffectContainer {
signInEntryContent
Expand Down
Loading
Loading