Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
17229 Sources/TerminalController.swift
15966 Sources/ContentView.swift
14501 Sources/AppDelegate.swift
13899 Sources/Workspace.swift
13951 Sources/Workspace.swift
13458 Sources/GhosttyTerminalView.swift
10607 Sources/Panels/BrowserPanel.swift
8286 Sources/cmuxApp.swift
Expand Down
8 changes: 8 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
A5001660 /* RestorableAgentSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001661 /* RestorableAgentSession.swift */; };
C0D3499C0D3499C0D349903 /* CodexSessionTitleParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3499C0D3499C0D349904 /* CodexSessionTitleParser.swift */; };
A5001650 /* CmuxConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001651 /* CmuxConfig.swift */; };
C10D00030000000000000003 /* CmuxSurfaceTabBarBuiltInAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10D00040000000000000004 /* CmuxSurfaceTabBarBuiltInAction.swift */; };
E30750000000000000000004 /* CmuxWorkspaceDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = E30750000000000000000003 /* CmuxWorkspaceDefinition.swift */; };
Expand Down Expand Up @@ -285,6 +286,7 @@
F4200000A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4200001A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift */; };
F4100000A1B2C3D4E5F60718 /* PortScannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4100001A1B2C3D4E5F60718 /* PortScannerTests.swift */; };
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */; };
C0D3499C0D3499C0D349901 /* RestorableCodexTitleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3499C0D3499C0D349902 /* RestorableCodexTitleTests.swift */; };
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */; };
F6001000A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6001001A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift */; };
F6100000A1B2C3D4E5F60718 /* WorkspaceRemoteConnectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6100001A1B2C3D4E5F60718 /* WorkspaceRemoteConnectionTests.swift */; };
Expand Down Expand Up @@ -571,6 +573,7 @@
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = "<group>"; };
A5001661 /* RestorableAgentSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableAgentSession.swift; sourceTree = "<group>"; };
C0D3499C0D3499C0D349904 /* CodexSessionTitleParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodexSessionTitleParser.swift; sourceTree = "<group>"; };
A5001651 /* CmuxConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxConfig.swift; sourceTree = "<group>"; };
C10D00040000000000000004 /* CmuxSurfaceTabBarBuiltInAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxSurfaceTabBarBuiltInAction.swift; sourceTree = "<group>"; };
E30750000000000000000003 /* CmuxWorkspaceDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CmuxWorkspaceDefinition.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -652,6 +655,7 @@
F4200001A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAppearanceSnapshotTests.swift; sourceTree = "<group>"; };
F4100001A1B2C3D4E5F60718 /* PortScannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortScannerTests.swift; sourceTree = "<group>"; };
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPersistenceTests.swift; sourceTree = "<group>"; };
C0D3499C0D3499C0D349902 /* RestorableCodexTitleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorableCodexTitleTests.swift; sourceTree = "<group>"; };
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateShortcutRoutingTests.swift; sourceTree = "<group>"; };
E3309A0A /* AppDelegateEqualizeSplitsShortcutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateEqualizeSplitsShortcutTests.swift; sourceTree = "<group>"; };
F6001001A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutUnbindingTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -969,6 +973,7 @@
A5001222 /* WindowAccessor.swift */,
A5001611 /* SessionPersistence.swift */,
A5001661 /* RestorableAgentSession.swift */,
C0D3499C0D3499C0D349904 /* CodexSessionTitleParser.swift */,
A5001641 /* RemoteRelayZshBootstrap.swift */,
A5001651 /* CmuxConfig.swift */,
C10D00040000000000000004 /* CmuxSurfaceTabBarBuiltInAction.swift */,
Expand Down Expand Up @@ -1033,6 +1038,7 @@
F4200001A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift */,
F4100001A1B2C3D4E5F60718 /* PortScannerTests.swift */,
F5000001A1B2C3D4E5F60718 /* SessionPersistenceTests.swift */,
C0D3499C0D3499C0D349902 /* RestorableCodexTitleTests.swift */,
FA100001A1B2C3D4E5F60718 /* BrowserImportMappingTests.swift */,
F6000001A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift */,
C34670010000000000000002 /* AppDelegateRenameShortcutContextTests.swift */,
Expand Down Expand Up @@ -1474,6 +1480,7 @@
A500120C /* WindowAccessor.swift in Sources */,
A5001610 /* SessionPersistence.swift in Sources */,
A5001660 /* RestorableAgentSession.swift in Sources */,
C0D3499C0D3499C0D349903 /* CodexSessionTitleParser.swift in Sources */,
A5001640 /* RemoteRelayZshBootstrap.swift in Sources */,
A5001650 /* CmuxConfig.swift in Sources */,
C10D00030000000000000003 /* CmuxSurfaceTabBarBuiltInAction.swift in Sources */,
Expand Down Expand Up @@ -1576,6 +1583,7 @@
F4200000A1B2C3D4E5F60718 /* WindowAppearanceSnapshotTests.swift in Sources */,
F4100000A1B2C3D4E5F60718 /* PortScannerTests.swift in Sources */,
F5000000A1B2C3D4E5F60718 /* SessionPersistenceTests.swift in Sources */,
C0D3499C0D3499C0D349901 /* RestorableCodexTitleTests.swift in Sources */,
FA100000A1B2C3D4E5F60718 /* BrowserImportMappingTests.swift in Sources */,
F6000000A1B2C3D4E5F60718 /* AppDelegateShortcutRoutingTests.swift in Sources */,
C34670010000000000000001 /* AppDelegateRenameShortcutContextTests.swift in Sources */,
Expand Down
64 changes: 64 additions & 0 deletions Sources/CodexSessionTitleParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

enum CodexSessionTitleParser {
static let launchSource = "surface-title"

private static let sessionSlugRegex = try! NSRegularExpression(
pattern: #"(?i)(?:^|[^A-Z0-9_-])(codex-[0-9a-f]{8,}(?:-[A-Z0-9]+)+)(?=$|[^A-Z0-9_-])"#
Comment on lines +3 to +7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing nonisolated on pure static helpers

CodexSessionTitleParser is a pure string-parsing utility with no actor-bound state. In a module with @MainActor-by-default isolation, all static members — launchSource, sessionSlugRegex, sessionId(from:), restorableSnapshot(fromTitle:workingDirectory:), isSurfaceTitleSnapshot(_:), snapshot(_:matchesTitle:), and normalized(_:) — will be implicitly @MainActor. That unnecessarily couples calls to them through the main actor, prevents use from background contexts, and violates the actor-isolation rule that pure helpers and file-scoped constants should be explicitly nonisolated. Marking each static member nonisolated (and the enum itself nonisolated to make the annotation structural rather than per-member) is the fix.

File Used: .github/review-bot-rules/swift-actor-isolation.md (source)

)
Comment on lines +6 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 try! on static regex initialization

The regex is a compile-time-constant literal so this will never trap in practice, but try! on a static let is fragile — any future edit to the pattern string (typo, feature addition) silently becomes a launch crash instead of a compile or test failure. Swift's native Regex literal (#/pattern/) validates the pattern at compile time and eliminates the forced try entirely; if that spelling isn't available on the OS minimum target, a static var with a do/catch and preconditionFailure gives a clear crash site and message.


static func sessionId(from title: String?) -> String? {
guard let title else { return nil }
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }

let range = NSRange(trimmed.startIndex..<trimmed.endIndex, in: trimmed)
guard let match = sessionSlugRegex.firstMatch(in: trimmed, range: range),
match.numberOfRanges > 1,
let slugRange = Range(match.range(at: 1), in: trimmed) else {
return nil
}
return String(trimmed[slugRange])
}

static func restorableSnapshot(
fromTitle title: String?,
workingDirectory: String?
) -> SessionRestorableAgentSnapshot? {
guard let sessionId = sessionId(from: title) else { return nil }
let normalizedWorkingDirectory = normalized(workingDirectory)
return SessionRestorableAgentSnapshot(
kind: .codex,
sessionId: sessionId,
workingDirectory: normalizedWorkingDirectory,
launchCommand: AgentLaunchCommandSnapshot(
launcher: "codex",
executablePath: "codex",
arguments: [
"codex",
"--dangerously-bypass-approvals-and-sandbox",
],
workingDirectory: normalizedWorkingDirectory,
environment: nil,
capturedAt: nil,
source: launchSource
)
)
}

static func isSurfaceTitleSnapshot(_ snapshot: SessionRestorableAgentSnapshot) -> Bool {
snapshot.kind == .codex && snapshot.launchCommand?.source == launchSource
}

static func snapshot(_ snapshot: SessionRestorableAgentSnapshot, matchesTitle title: String?) -> Bool {
isSurfaceTitleSnapshot(snapshot) && sessionId(from: title) == snapshot.sessionId
}

private static func normalized(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
return nil
}
return trimmed
}
}
56 changes: 54 additions & 2 deletions Sources/Workspace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ extension Workspace {
restoredTerminalScrollbackByPanelId.removeAll(keepingCapacity: false)
restoredAgentSnapshotsByPanelId.removeAll(keepingCapacity: false)
restoredAgentAutoResumePendingPanelIds.removeAll(keepingCapacity: false)
restoredAgentAutoResumeRunningPanelIds.removeAll(keepingCapacity: false)
invalidatedRestoredAgentFingerprintsByPanelId.removeAll(keepingCapacity: false)

let normalizedCurrentDirectory = snapshot.currentDirectory.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down Expand Up @@ -370,10 +371,29 @@ extension Workspace {
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}
let effectiveRestorableAgent = restoredAgentSnapshotsByPanelId[panelId]

let panelTitle = panelTitle(panelId: panelId)
let customTitle = panelCustomTitles[panelId]
let shellActivityState = panelShellActivityStates[panelId] ?? .unknown
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Title snapshot overwrites richer hook-based restored snapshots

Medium Severity

When restorableAgent is nil (hook state missing) and the shell isn't at prompt idle, the else if branch unconditionally overwrites restoredAgentSnapshotsByPanelId[panelId] with a title-based snapshot — even when there's already a richer hook-based snapshot from a session restore. This replaces detailed launch command data (actual executable path, real arguments, environment) with hardcoded values ("codex" path and --dangerously-bypass-approvals-and-sandbox). A guard checking that no existing non-title snapshot is present would prevent this data loss.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2b46dc8. Configure here.

}
Comment on lines +378 to +395

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 State mutation as a side-effect of snapshot reads

The new restorableAgent == nil block mutates restoredAgentSnapshotsByPanelId, restoredAgentAutoResumePendingPanelIds, restoredAgentAutoResumeRunningPanelIds, and invalidatedRestoredAgentFingerprintsByPanelId inside what is otherwise a read-path snapshot computation. This extends an already present (but limited) mutation in the restorableAgent != nil branch. The practical consequence is that sessionSnapshot() is not idempotent — the first call when shellActivityState == .promptIdle clears the surface-title snapshot, and a second call then observes different state — and the source of truth for "does this panel have a title-derived codex snapshot?" is now spread across three different call sites (updatePanelTitle, updatePanelShellActivityState, and sessionPanelSnapshot). These transitions belong in the updatePanelTitle/updatePanelShellActivityState handlers so the snapshot function is a pure reader of already-correct state.

File Used: .github/review-bot-rules/swift-architectural-rethink.md (source)

Comment on lines +378 to +395

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale title-derived snapshots when the live title stops matching.

If hook state never comes back after restore, panelShellActivityStates[panelId] can stay .unknown. In that case this block only clears surface-title snapshots on .promptIdle, so once the panel title stops parsing as a Codex slug the old snapshot is kept and re-persisted on the next workspace save. That recreates the stale auto-resume loop this PR is trying to eliminate.

Suggested fix
         if restorableAgent == nil {
             let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
             if shellActivityState == .promptIdle,
                let currentSnapshot,
                CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
                 restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
                 restoredAgentAutoResumePendingPanelIds.remove(panelId)
                 restoredAgentAutoResumeRunningPanelIds.remove(panelId)
                 invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
             } else if shellActivityState != .promptIdle,
                       let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
                         fromTitle: panelTitles[panelId] ?? panel.displayTitle,
                         workingDirectory: panelDirectories[panelId]
                       ) {
                 restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
                 invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
+            } else if let currentSnapshot,
+                      CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
+                restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
+                restoredAgentAutoResumePendingPanelIds.remove(panelId)
+                restoredAgentAutoResumeRunningPanelIds.remove(panelId)
+                invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
             }
         }
📝 Committable suggestion

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

Suggested change
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}
if restorableAgent == nil {
let currentSnapshot = restoredAgentSnapshotsByPanelId[panelId]
if shellActivityState == .promptIdle,
let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if shellActivityState != .promptIdle,
let titleRestorableAgent = CodexSessionTitleParser.restorableSnapshot(
fromTitle: panelTitles[panelId] ?? panel.displayTitle,
workingDirectory: panelDirectories[panelId]
) {
restoredAgentSnapshotsByPanelId[panelId] = titleRestorableAgent
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else if let currentSnapshot,
CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Workspace.swift` around lines 378 - 395, When restorableAgent is nil,
add handling so a stored surface-title snapshot is cleared if the live title no
longer parses as a Codex restorable snapshot even when the hook state is
.unknown: inside the existing block that checks
restoredAgentSnapshotsByPanelId[panelId], keep the current branch that clears on
shellActivityState == .promptIdle, and in the else-if branch (the
shellActivityState != .promptIdle branch that currently tries
CodexSessionTitleParser.restorableSnapshot(...)), add logic so that if the
parser returns nil AND the existing currentSnapshot is a surface-title snapshot
(use CodexSessionTitleParser.isSurfaceTitleSnapshot(currentSnapshot)), you
remove restoredAgentSnapshotsByPanelId[panelId],
restoredAgentAutoResumePendingPanelIds.remove(panelId),
restoredAgentAutoResumeRunningPanelIds.remove(panelId), and
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId); this
ensures stale title-derived snapshots are cleared when the live title stops
matching even if panelShellActivityStates[panelId] remains .unknown.

let effectiveRestorableAgent = restoredAgentSnapshotsByPanelId[panelId]
let directory = panelDirectories[panelId] ?? effectiveRestorableAgent?.workingDirectory
let isPinned = pinnedPanelIds.contains(panelId)
let isManuallyUnread = manualUnreadPanelIds.contains(panelId)
Expand Down Expand Up @@ -648,13 +668,16 @@ extension Workspace {
restoredAgentSnapshotsByPanelId[terminalPanel.id] = restorableAgent
if restoredAgentResumeInput != nil {
restoredAgentAutoResumePendingPanelIds.insert(terminalPanel.id)
restoredAgentAutoResumeRunningPanelIds.remove(terminalPanel.id)
} else {
restoredAgentAutoResumePendingPanelIds.remove(terminalPanel.id)
restoredAgentAutoResumeRunningPanelIds.remove(terminalPanel.id)
}
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: terminalPanel.id)
} else {
restoredAgentSnapshotsByPanelId.removeValue(forKey: terminalPanel.id)
restoredAgentAutoResumePendingPanelIds.remove(terminalPanel.id)
restoredAgentAutoResumeRunningPanelIds.remove(terminalPanel.id)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: terminalPanel.id)
}
applySessionPanelMetadata(snapshot, toPanelId: terminalPanel.id)
Expand Down Expand Up @@ -7267,6 +7290,7 @@ final class Workspace: Identifiable, ObservableObject {
private var restoredTerminalScrollbackByPanelId: [UUID: String] = [:]
private var restoredAgentSnapshotsByPanelId: [UUID: SessionRestorableAgentSnapshot] = [:]
private var restoredAgentAutoResumePendingPanelIds: Set<UUID> = []
private var restoredAgentAutoResumeRunningPanelIds: Set<UUID> = []
private var invalidatedRestoredAgentFingerprintsByPanelId: [UUID: Int] = [:]
private var pendingTerminalInputObserversByPanelId: [UUID: [WorkspacePendingTerminalInputObserver]] = [:]

Expand Down Expand Up @@ -8554,15 +8578,38 @@ final class Workspace: Identifiable, ObservableObject {
guard previousState != state else { return }
panelShellActivityStates[panelId] = state
if state == .commandRunning, let restoredAgent = restoredAgentSnapshotsByPanelId[panelId] {
if restoredAgentAutoResumePendingPanelIds.remove(panelId) == nil {
if restoredAgentAutoResumePendingPanelIds.remove(panelId) != nil {
restoredAgentAutoResumeRunningPanelIds.insert(panelId)
} else if CodexSessionTitleParser.snapshot(
restoredAgent,
matchesTitle: panelTitles[panelId] ?? panels[panelId]?.displayTitle
) {
restoredAgentAutoResumeRunningPanelIds.insert(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
} else {
let fingerprint = TabManager.restorableAgentSnapshotFingerprint(restoredAgent)
invalidatedRestoredAgentFingerprintsByPanelId[panelId] = fingerprint
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
#if DEBUG
cmuxDebugLog(
"session.restore.agent.invalidate panel=\(panelId.uuidString.prefix(5)) " +
"kind=\(restoredAgent.kind.rawValue) session=\(restoredAgent.sessionId.prefix(8))"
)
#endif
}
} else if state == .promptIdle {
let wasAutoResume = restoredAgentAutoResumeRunningPanelIds.remove(panelId) != nil
let wasPendingAutoResume = restoredAgentAutoResumePendingPanelIds.remove(panelId) != nil
if let restoredAgent = restoredAgentSnapshotsByPanelId[panelId],
wasAutoResume || wasPendingAutoResume || CodexSessionTitleParser.isSurfaceTitleSnapshot(restoredAgent) {
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
#if DEBUG
cmuxDebugLog(
"session.restore.agent.clear panel=\(panelId.uuidString.prefix(5)) " +
"kind=\(restoredAgent.kind.rawValue) session=\(restoredAgent.sessionId.prefix(8))"
)
#endif
}
}
Expand Down Expand Up @@ -8796,6 +8843,9 @@ final class Workspace: Identifiable, ObservableObject {
restoredAgentAutoResumePendingPanelIds = restoredAgentAutoResumePendingPanelIds.filter {
validSurfaceIds.contains($0)
}
restoredAgentAutoResumeRunningPanelIds = restoredAgentAutoResumeRunningPanelIds.filter {
validSurfaceIds.contains($0)
}
invalidatedRestoredAgentFingerprintsByPanelId = invalidatedRestoredAgentFingerprintsByPanelId.filter {
validSurfaceIds.contains($0.key)
}
Expand Down Expand Up @@ -10538,6 +10588,7 @@ final class Workspace: Identifiable, ObservableObject {
pendingRemoteTerminalChildExitSurfaceIds.removeAll(keepingCapacity: false)
pruneSurfaceMetadata(validSurfaceIds: [])
restoredTerminalScrollbackByPanelId.removeAll(keepingCapacity: false)
restoredAgentAutoResumeRunningPanelIds.removeAll(keepingCapacity: false)
pendingTerminalInputObserversByPanelId.removeAll(keepingCapacity: false)
terminalInheritanceFontPointsByPanelId.removeAll(keepingCapacity: false)
lastTerminalConfigInheritancePanelId = nil
Expand Down Expand Up @@ -13302,6 +13353,7 @@ extension Workspace: BonsplitDelegate {
restoredTerminalScrollbackByPanelId.removeValue(forKey: panelId)
restoredAgentSnapshotsByPanelId.removeValue(forKey: panelId)
restoredAgentAutoResumePendingPanelIds.remove(panelId)
restoredAgentAutoResumeRunningPanelIds.remove(panelId)
invalidatedRestoredAgentFingerprintsByPanelId.removeValue(forKey: panelId)
PortScanner.shared.unregisterPanel(workspaceId: id, panelId: panelId)
terminalInheritanceFontPointsByPanelId.removeValue(forKey: panelId)
Expand Down
Loading