@@ -458,21 +458,37 @@ class TerminalController {
458458 }
459459 }
460460
461+ /// Returns `true` when the incoming state differs from the last *applied* state,
462+ /// meaning the report is worth dispatching to the main thread.
463+ ///
464+ /// This method only reads the dedup dict; it does NOT record the new state.
465+ /// Call `recordShellActivity` after the update is confirmed applied on the main
466+ /// thread so that a failed apply (panel absent) never suppresses the next
467+ /// identical report.
461468 func shouldPublishShellActivity(
462469 workspaceId: UUID,
463470 panelId: UUID,
464471 state: Workspace.PanelShellActivityState
465472 ) -> Bool {
466473 let key = SocketSurfaceKey(workspaceId: workspaceId, panelId: panelId)
467474 return queue.sync {
468- if lastReportedShellStates[key] == state {
469- return false
470- }
471- if lastReportedShellStates.count >= maxTrackedShellStates {
472- lastReportedShellStates.removeAll(keepingCapacity: true)
475+ lastReportedShellStates[key] != state
476+ }
477+ }
478+
479+ /// Records that the given state was successfully applied to the given panel.
480+ /// Must be called only when `updateSurfaceShellActivity` returned `true`.
481+ func recordShellActivity(
482+ workspaceId: UUID,
483+ panelId: UUID,
484+ state: Workspace.PanelShellActivityState
485+ ) {
486+ let key = SocketSurfaceKey(workspaceId: workspaceId, panelId: panelId)
487+ queue.async {
488+ if self.lastReportedShellStates.count >= self.maxTrackedShellStates {
489+ self.lastReportedShellStates.removeAll(keepingCapacity: true)
473490 }
474- lastReportedShellStates[key] = state
475- return true
491+ self.lastReportedShellStates[key] = state
476492 }
477493 }
478494 }
@@ -15366,16 +15382,35 @@ class TerminalController {
1536615382 }
1536715383
1536815384 if let scope = Self.explicitSocketScope(options: parsed.options) {
15385+ // Fast-path check: skip dispatch if we already know this state is current.
15386+ // Note: we only READ here, not record. Recording happens after the update is
15387+ // confirmed applied on the main thread, preventing a race where the panel
15388+ // isn't registered yet and the dedup would suppress subsequent reports.
1536915389 guard Self.socketFastPathState.shouldPublishShellActivity(
1537015390 workspaceId: scope.workspaceId,
1537115391 panelId: scope.panelId,
1537215392 state: state
1537315393 ) else {
1537415394 return "OK"
1537515395 }
15396+ let fastPathState = Self.socketFastPathState
1537615397 DispatchQueue.main.async {
1537715398 guard let tabManager = AppDelegate.shared?.tabManagerFor(tabId: scope.workspaceId) else { return }
15378- tabManager.updateSurfaceShellActivity(tabId: scope.workspaceId, surfaceId: scope.panelId, state: state)
15399+ let applied = tabManager.updateSurfaceShellActivity(
15400+ tabId: scope.workspaceId,
15401+ surfaceId: scope.panelId,
15402+ state: state
15403+ )
15404+ // Only record the state in the dedup dict when the update actually
15405+ // applied (panel was registered). If the panel was absent, we do NOT
15406+ // record, so the next identical report will not be suppressed.
15407+ if applied {
15408+ fastPathState.recordShellActivity(
15409+ workspaceId: scope.workspaceId,
15410+ panelId: scope.panelId,
15411+ state: state
15412+ )
15413+ }
1537915414 }
1538015415 return "OK"
1538115416 }
0 commit comments