Skip to content

Fix title-churn beachball in transcript adoption and sidebar rows - #6460

Merged
austinywang merged 19 commits into
mainfrom
issue-6459-beachball-title-churn-sidebar
Jun 20, 2026
Merged

austinywang merged 19 commits into
mainfrom
issue-6459-beachball-title-churn-sidebar

Conversation

@austinywang

@austinywang austinywang commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6459
Closes #5392

This fixes the main-thread beachball caused by terminal title churn feeding two expensive main-thread paths:

  1. AgentChatTranscriptService.observeAgentTitleChanges synchronously routed every Claude title update through workspace-wide adoption, and adoptDetectedClaudeSession could scan the Claude project directory and parse the newest transcript on the main actor.
  2. VerticalTabsSidebar passed global selected-workspace state through the row builder, so focus/selection changes made the whole ForEach depend on the same AttributeGraph selection node instead of letting unchanged rows stay behind TabItemView.equatable().

Changes

  • Debounce and coalesce title-change adoption by terminal surface using the existing burst coalescer, so title churn queues one latest pending adoption per surface without a per-title sleep task.
  • Cache delivered title keys per surface so repeated generic Claude title churn does not keep invoking adoption.
  • Route live title changes to a surface-scoped TerminalController.adoptDetectedAgentSession(titleChange:) path instead of scanning every terminal in the workspace.
  • Keep adoptDetectedClaudeSession main-actor work to cheap registry updates and scheduling. Claude transcript discovery/parsing now runs in a cancellable detached utility task, then hops back to the main actor only to apply a still-current result.
  • Skip transcript work entirely once a live surface already has a transcript-bound session; provisional/no-transcript sessions are throttled and deduped by surface/input key.
  • Move sidebar active-row state into TabItemView via a row-local selectedTabIdPublisher boolean projection. The parent no longer passes isActive, so unchanged rows keep the same equatable inputs when selection changes.

Testability / verification

This is a main-thread timing/perf bug, so I did not fabricate a unit test. The meaningful verification is the user-controlled dev build + arch -arm64 lldb repro from #6459: split a workspace, rapidly switch pane focus, and confirm the 140% CPU spike and ModifiedViewList.applyNodes / recursive swift_release teardown are gone.

Per request, I did not run a local build, reload.sh, xcodebuild, or local tests. CI is the verification bar for compilation/checks on this branch.

Localization

No user-facing strings were added or changed.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Fixes the main-thread beachball from Claude title churn and stops sidebar rows from redrawing on selection changes. Moves to per-surface, coalesced title handling and resolves transcripts off the main thread with safer live-session indexing and claim management.

  • Bug Fixes

    • Coalesced per-surface “✳ Claude” title changes (250ms) and routed adoption through TerminalController.adoptDetectedAgentSession(titleChange:), avoiding workspace-wide sweeps; cleared per-surface state and canceled scans on title changes or session end.
    • Resolved transcripts off the main thread per surface with throttling (4s), deduping, and bounded forced retries (3); applied results only if the key still matches; preferred exact title-hints over newer untitled files; ignored $HOME; cancellable.
    • Tracked the newest non-ended session per surface with cache revalidation and dead-PID eviction; retained detected transcript-claim IDs while the session is live and released them on end to prevent cross-surface collisions; retried on bound-collision.
    • Sidebar: made row selection local via selectedTabIdPublisher so unchanged rows remain Equatable and don’t redraw on selection changes.
  • Refactors

    • Split title detection/resolution scheduling into AgentChatTranscriptService+TitleDetection.swift; made AgentChatTranscriptResolver Sendable.

Written for commit e906242. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved agent chat transcript adoption on mobile by forwarding detected title changes to the correct terminal surface, improving session/transcript binding during startup and updates.
    • Enhanced live session selection by tracking the newest non-ended session per terminal surface to avoid incorrect or stale bindings.
  • Performance
    • Reduced redundant transcript detection and resolution using per-surface throttling, coalesced title changes, deduped scheduling, and forced retries when needed.
    • Improved sidebar responsiveness by localizing workspace row “active” state to reduce unnecessary UI re-renders.

@vercel

vercel Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 20, 2026 12:38am
cmux-staging Building Building Preview, Comment Jun 20, 2026 12:38am

@coderabbitai

coderabbitai Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR addresses two root causes of the main-thread beachball (#6459): it refactors AgentChatTranscriptService to debounce per-surface title-change adoption and run transcript scanning asynchronously via cancellable Task.detached with key-based deduplication, and it rewires TabItemView to track active state locally via @State and selectedTabIdPublisher instead of relying on a parent-provided isActive that caused full sidebar ForEach rebuilds.

Changes

Agent Transcript Adoption Pipeline

Layer / File(s) Summary
AgentChatTranscriptResolver: Sendable + local FileManager + cancellation checks
Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift
Conforms AgentChatTranscriptResolver to Sendable by removing the stored FileManager instance property; each method now creates a method-local FileManager.default value, enabling use from Task.detached. Refactors newestClaudeTranscript to use an imperative loop with per-entry cancellation checks for prompt Task cancellation. Reworks claudeTranscriptTitle(at:) to stream JSONL files via FileHandle with chunked reads instead of whole-file loads.
AgentChatSessionRegistry: live session index and lookup
Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift
Adds liveSessionIDBySurfaceID index dictionary; implements liveSession(surfaceID:) that sweeps dead processes and returns the newest non-ended record for a surface; maintains the index across mutations (update, seedFromHookStores, adoptDetectedSession, noteHookEvent) via index-maintenance helpers.
AgentChatTranscriptService: per-surface state and updated start API
Sources/Mobile/AgentChat/AgentChatTranscriptService.swift
Expands stored scheduling state with per-surface pending title changes, debounce tasks, delivered keys, in-flight resolution tasks/keys, and claimedDetectedTranscriptSessionIDs; updates start to accept a (GhosttyTitleChange) -> Bool callback replacing prior tab-id-only wiring. Introduces PendingTitleChange and ClaudeTranscriptResolutionKey type aliases.
AgentChatTranscriptService+TitleDetection: debounce, throttle, async resolution
Sources/Mobile/AgentChat/AgentChatTranscriptService+TitleDetection.swift, cmux.xcodeproj/project.pbxproj
Implements scheduleTitleDetectedAdoption/flushTitleDetectedAdoption for per-surface debounced title adoption with bounded debounce and "delivered" tracking; adds scheduleClaudeTranscriptResolution for throttled, cancellable Task.detached transcript resolution with per-surface deduplication; implements applyClaudeTranscriptResolution for key-guarded result application and re-scheduling logic on session conflicts; adds scheduleForcedClaudeTranscriptRetry for bounded retry. Registers the TitleDetection source file in the Xcode project build phases.
AgentChatTranscriptService: adoptDetectedClaudeSession refactor and end-state cleanup
Sources/Mobile/AgentChat/AgentChatTranscriptService.swift
Uses liveSession(surfaceID:) to update existing bound sessions in place and short-circuit when transcript is already bound; defers transcript binding to scheduleClaudeTranscriptResolution; provisional session creation now explicitly schedules resolution instead of resolving inline. Updates Claude title detection with "generic:claude" fallback; clears title-detection state on session end; unions claimed-session exclusion set.
TerminalController: surface-scoped entry point and panel detection helper
Sources/TerminalController+MobileChat.swift
Adds adoptDetectedAgentSession(titleChange:) as a surface-scoped entry point that resolves workspace and terminal surface from a GhosttyTitleChange; refactors adoptDetectedAgentSessions(workspace:) to delegate per-panel detection/adoption to a new private helper encapsulating Claude detection heuristics (launch/context/title patterns) and working-directory resolution.
AppDelegate: wire surface-scoped adoption callback
Sources/AppDelegate.swift
Updates agentChatTranscriptService.start to pass the new adoptDetectedAgentSession(titleChange:) callback replacing the prior adoptDetectedAgentSessions(workspaceID:) call.

Sidebar Active-State Scoping

Layer / File(s) Summary
TabItemView: local active-state tracking and reduced re-rendering
Sources/ContentView.swift
Removes parent-provided isActive from TabItemView constructor and from its Equatable comparison; adds @State observedIsActive initialized on onAppear and updated via selectedTabIdPublisher with removeDuplicates; computed isActive uses observedIsActive with fallback to tabManager.selectedTabId == tab.id; adds updateObservedActiveState conditional update helper to avoid redundant state changes.

Sequence Diagram

sequenceDiagram
  participant TitleObs as Title Observer
  participant Service as AgentChatTranscriptService
  participant TC as TerminalController
  participant Detached as Task.detached
  participant Registry as AgentChatSessionRegistry

  TitleObs->>Service: title-change notification
  Service->>Service: scheduleTitleDetectedAdoption(titleChange)
  Note over Service: debounce per surface, cancel prior timer
  Service->>Service: timer expires → flushTitleDetectedAdoption
  Service->>TC: adoptDetectedAgentSession(titleChange:) → Bool
  TC->>Service: adoptDetectedClaudeSession(workspaceID:surfaceID:...)
  Service->>Registry: liveSession(surfaceID:)
  Registry-->>Service: existing record or nil
  Service->>Service: scheduleClaudeTranscriptResolution(key, surfaceID)
  Note over Service: cancel prior task if key changed
  Service->>Detached: async newestClaudeTranscript
  Detached-->>Service: resolved transcript path or nil
  Service->>Service: applyClaudeTranscriptResolution(guard: key current)
  alt bound session exists, transcript unbound
    Service->>Registry: update session transcript binding
  else no bound session
    Service->>Registry: adopt newly resolved session
    Note over Service: re-schedule if session incompatible
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • manaflow-ai/cmux#6216: Introduced the non-singleton AgentChatTranscriptService ownership model and the initial start(adoptDetectedAgentSessions:) callback API that this PR's start(adoptDetectedAgentSession:) refactor directly extends.
  • manaflow-ai/cmux#4736: Modified TabItemView and its active-state handling in Sources/ContentView.swift, directly overlapping with this PR's sidebar scoping changes.

Poem

🐇 Hop hop, no more beachball spin,
The transcript scans moved off main-thread thin.
Each sidebar row now watches itself,
No ForEach storm to peg the shelf.
Debounce the churn, let async win! ✨


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Cache Substitution Correctness ❌ Error AgentChatSessionRegistry.liveSession() line 77 returns a cached sessionID without revalidating a dead PID after ending it, violating cache substitution correctness rules for stale cache handling. Replace line 77 with recursive call: return liveSession(surfaceID: surfaceID) to revalidate any replacement from the rebuilt index, per the review comment fix.
Cmux Swiftui State Layout ❌ Error TabItemView (a LazyVStack row in VerticalTabsSidebar) holds @StateObject private var contextMenuState, an ObservableObject store reference, violating the snapshot-boundary rule documented in CLAUDE... Replace @StateObject contextMenuState with a value snapshot + action closures passed from parent, per snapshot-boundary pattern in IndexSectionActions/SectionGapActions (SessionIndexView.swift).
Cmux Architecture Rethink ❌ Error PR introduces timing-based repair via NotificationBurstCoalescer (DispatchQueue.main.asyncAfter) + multiple mutable per-surface state dicts (detectionScanAt, pendingTitleChanges, transcriptResolu... Consolidate per-surface adoption/transcript-resolution state into a single source of truth (e.g., reactive record w/ adoption phase); eliminate DispatchQueue.main.asyncAfter by routing title changes directly to adoption handlers; ensure...
Docstring Coverage ⚠️ Warning Docstring coverage is 42.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main changes: fixing beachball caused by title churn and sidebar row issues, which matches the core objectives of resolving main-thread bottlenecks from transcript adoption and sidebar rendering.
Linked Issues check ✅ Passed The PR implementation fully addresses both linked issues: #6459 (main-thread beachball from title-churn and sidebar ForEach) and #5392 (CPU pinning with Claude sessions). All coding requirements are met via debounced adoption, off-main transcript resolution, per-surface indexing, and sidebar state scoping.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the two bottlenecks identified in linked issues: title-change coalescing, off-main transcript resolution, per-surface indexing, and sidebar state scoping. No unrelated modifications are present.
Cmux Swift Actor Isolation ✅ Passed PR correctly implements Swift 6 actor isolation: AgentChatTranscriptResolver made Sendable with no mutable state; background transcript resolution properly detached with Sendable closure and MainAc...
Cmux Swift Blocking Runtime ✅ Passed No blocking primitives introduced. PR uses proper async/await with Task.detached for off-main-thread work, time-interval throttling (non-blocking), and cancellation-aware patterns without semaphore...
Cmux Expensive Synchronous Load ✅ Passed PR moves expensive Claude transcript discovery and parsing OFF the main actor into cancellable Task.detached(.utility) jobs, then applies results only on main thread if still current. No expensive...
Cmux No Hacky Sleeps ✅ Passed Check is not applicable: the rule targets TypeScript, JavaScript, shell, and non-Swift build/runtime scripts. This PR modifies only Swift code and Xcode config; Swift timing is covered by a separat...
Cmux Algorithmic Complexity ✅ Passed PR improves algorithmic complexity: moves expensive transcript resolution off-main, adds O(1) indexed session lookup (vs prior O(n) scans), debounces title changes, and scopes sidebar rendering to...
Cmux Swift Concurrency ✅ Passed PR introduces Task.detached for off-main transcript resolution (properly stored, cancelled, and managed with main-actor re-entry via withTaskCancellationHandler). ContentView TabItemView adds .onRe...
Cmux Swift @Concurrent ✅ Passed The PR correctly uses @concurrent @Sendable annotations. In AgentChatTranscriptService+TitleDetection.swift (lines 86-104), the @concurrent boundary properly marks nonisolated async work that leave...
Cmux Swift File And Package Boundaries ✅ Passed All files meet Swift file and package boundary rules: new 222-line TitleDetection extension has clear single responsibility (title-detected adoption); oversized files (AppDelegate 17606 lines, Cont...
Cmux Swiftpm Lockfiles ✅ Passed PR includes root Xcode Package.resolved and all 11 package-local lockfiles with no .gitignore violations; cmux-owned packages only ignore .build/, not Package.resolved.
Cmux Swift Logging ✅ Passed No logging violations detected. PR adds/modifies 0 print/NSLog statements and contains no unsafe Logger declarations, file logging, or data exposure in 7 modified files.
Cmux User-Facing Error Privacy ✅ Passed PR adds 5 user-facing error messages in TerminalController+MobileChat.swift, all localized and compliant: user-actionable text with no vendor/provider names, credentials, or implementation details...
Cmux Full Internationalization ✅ Passed No user-facing strings were added or modified in this PR. All changes are backend refactoring (off-main-thread transcript resolution, title debouncing, state management). No string catalogs or web...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR makes no additions to NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup. All changes are internal service implementations (transcript handling, session registry, RPC routing)...
Cmux Source Artifacts ✅ Passed All 8 changed files are legitimate: 7 hand-written Swift source files and 1 Xcode .pbxproj build config. No artifact directories, generated logs, caches, or local tool output present.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR introduces no new test/debug seams. All added public methods (liveSession, adoptDetectedAgentSession) are product functionality, not test observability accessors. The #if compiler(>=6.2) gate is...
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all key template sections with detailed explanations of the changes and their motivation.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6459-beachball-title-churn-sidebar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

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

Inline comments:
In `@Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift`:
- Around line 66-69: The liveSession(surfaceID:) method performs an O(n) scan of
all records on every call, which degrades performance at scale. Create a private
dictionary property that indexes live sessions by surfaceID (mapping surfaceID
strings to sessionID keys for sessions in non-ended state). Update this index
whenever the session state changes: in the update method when updating session
state, in adoptDetectedSession when adding new sessions, in noteHookEvent when
state transitions occur, and in sweepDeadProcesses after removing dead sessions.
Then refactor the liveSession method to use this index dictionary for O(1)
lookup instead of scanning records.values, ensuring the index stays in sync with
the actual live sessions.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift`:
- Around line 285-293: The deliveredTitleKeys cache in the
scheduleTitleDetectedAdoption method is never cleared when Claude becomes
inactive on a surface, causing new generic Claude titles to be suppressed
indefinitely on reused surfaces. You need to clear the
deliveredTitleKeys[surfaceID] entry (and associated pending state in
pendingTitleChanges[surfaceID]) in two scenarios: first, when a non-Claude title
arrives for that surface in the scheduling logic, and second, when the bound
session ends or is deallocated. This ensures the cache is reset when the surface
context changes, allowing new Claude title detections to be properly processed.
- Around line 401-419: When an existing bound session is found for the surface
(in the first branch handling `if let bound = registry.liveSession`), the code
updates the bound session with the resolved transcript path but fails to claim
the `resolved.sessionID`. This violates the resolver's session-id exclusion
contract, allowing another surface to later adopt the same transcript. Fix this
by rekeying or merging the provisional record to use `resolved.sessionID`
instead of `bound.sessionID` before returning, ensuring the resolved session ID
is properly claimed and marked as used in the registry.
🪄 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: 9379e3bd-81c0-4c4e-9ff0-bfe1415f66c9

📥 Commits

Reviewing files that changed from the base of the PR and between ed00998 and 27d747d.

📒 Files selected for processing (6)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptService.swift
  • Sources/TerminalController+MobileChat.swift

Comment thread Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift
Comment thread Sources/Mobile/AgentChat/AgentChatTranscriptService.swift Outdated
Comment thread Sources/Mobile/AgentChat/AgentChatTranscriptService.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
Sources/Mobile/AgentChat/AgentChatTranscriptService.swift (4)

537-540: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not let stale ended records clear a newer surface occupant’s state.

This cleanup is keyed only by surfaceID. If an old session’s .ended update arrives after a new provisional Claude session has already been created on the same surface, clearTitleDetectionState cancels the new session’s pending transcript resolution. Only clear when there is no current non-ended live session for that surface.

Suggested patch direction
 if stateChanged, record.state == .ended {
-    if let surfaceID = record.surfaceID {
+    if let surfaceID = record.surfaceID,
+       registry.liveSession(surfaceID: surfaceID)?.sessionID == nil {
         clearTitleDetectionState(surfaceID: surfaceID)
     }

As per coding guidelines, per-surface transcript/session caches need stale-cache handling when titles/agents change.

🤖 Prompt for 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.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift` around lines 537 -
540, The clearTitleDetectionState call in the stateChanged and record.state ==
.ended check is keyed only by surfaceID without verifying if a newer active
session already exists on that surface, causing stale ended records to
incorrectly cancel newer provisional sessions. Before calling
clearTitleDetectionState with the surfaceID, add a check to confirm there is no
current non-ended live session for that surface ID, and only proceed with the
cleanup if that condition is met. This ensures stale session cleanup respects
active surface occupants that may have been created after the old session ended.

Source: Coding guidelines


444-455: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only claim a resolved transcript after it is actually bound.

Line 445 marks resolved.sessionID as claimed before checking whether the live surface already has a transcript. If that guard returns because another path bound a different transcript first, this result is never used but remains excluded from future detection. Move the claim to the branches that bind/adopt the resolved path, or only keep it when the existing bound path matches resolved.path.

Suggested patch direction
 detectionScanAt.removeValue(forKey: surfaceID)
-claimedDetectedTranscriptSessionIDs.insert(resolved.sessionID)
 if let bound = registry.liveSession(surfaceID: surfaceID) {
-    guard bound.transcriptPath == nil else { return }
+    if let existingPath = bound.transcriptPath {
+        if existingPath == resolved.path {
+            claimedDetectedTranscriptSessionIDs.insert(resolved.sessionID)
+        }
+        return
+    }
+    claimedDetectedTranscriptSessionIDs.insert(resolved.sessionID)
     registry.update(sessionID: bound.sessionID) { record in
         record.workspaceID = workspaceID
         record.surfaceID = surfaceID
         record.workingDirectory = workingDirectory
         record.transcriptPath = resolved.path
@@
 }
 
+claimedDetectedTranscriptSessionIDs.insert(resolved.sessionID)
 registry.adoptDetectedSession(

As per coding guidelines, cached transcript/session adoption state must not silently substitute stale or incorrect identity data.

Also applies to: 457-465

🤖 Prompt for 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.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift` around lines 444 -
455, The claimedDetectedTranscriptSessionIDs collection is marking
resolved.sessionID as claimed on line 445 before verifying whether the
transcript will actually be bound to the session. If the guard condition on the
next line returns early due to an existing transcript path, the sessionID
remains incorrectly marked as claimed even though the resolved path was never
used. Move the insertion of resolved.sessionID into
claimedDetectedTranscriptSessionIDs to only occur after confirming the resolved
path will be bound to the session, specifically within the registry.update call
block where the transcriptPath is actually being set, or add an additional
condition to only claim when the resolved path matches an existing bound path.

Source: Coding guidelines


344-350: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include the workspace/current binding in the resolution freshness guard.

Line 350 dedupes scans without workspaceID, but the task still captures workspaceID and later writes it back. If a surface is re-adopted or moved to another workspace while the scan is in flight and the working directory/title key are unchanged, the stale task can restore the old workspace binding. Add workspaceID to ClaudeTranscriptResolutionKey and/or guard against the current live binding before applying.

Suggested patch direction
 private typealias ClaudeTranscriptResolutionKey = (
+    workspaceID: String,
     workingDirectory: String,
     claimedSessionIDs: Set<String>,
     titleKey: String?,
     forceScan: Bool
 )
@@
 let key: ClaudeTranscriptResolutionKey = (
+    workspaceID: workspaceID,
     workingDirectory: workingDirectory,
     claimedSessionIDs: claimed,
     titleKey: Self.specificClaudeTitleKey(titleHint),
     forceScan: forceScan
 )
@@
 guard transcriptResolutionKeys[surfaceID] == key else {
     return
 }
+if let current = registry.liveSession(surfaceID: surfaceID),
+   current.workspaceID != workspaceID || current.workingDirectory != workingDirectory {
+    return
+}

As per coding guidelines, transcript/session adoption caches must handle stale-cache failures in correctness-sensitive flows.

Also applies to: 378-385, 448-452

🤖 Prompt for 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.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift` around lines 344 -
350, The ClaudeTranscriptResolutionKey used in the freshness guard does not
include workspaceID, which can cause stale cache issues when a surface is
re-adopted or moved to another workspace. Add workspaceID to the
ClaudeTranscriptResolutionKey tuple definition (alongside workingDirectory,
claimedSessionIDs, titleKey, and forceScan) to ensure the deduplication guard
properly handles workspace changes. Additionally, add a guard check to verify
the current live workspace binding matches before applying a cached resolution,
preventing stale workspace bindings from being restored when the surface binding
has changed.

Source: Coding guidelines


611-641: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not force-scan on volatile Claude title text.

specificClaudeTitleKey currently uses the full normalized title for any ✳-prefixed or Claude-containing title except a couple of generic forms. Spinner/token/elapsed-title churn can therefore become a new specific:* key each update, making isSpecificClaudeTitle true and bypassing detectionScanThrottle. Collapse volatile status titles to "generic:claude" unless a stable session/project identifier is extracted.

Suggested patch direction
 private static func specificClaudeTitleKey(_ title: String?) -> String? {
     guard var title = title?.trimmingCharacters(in: .whitespacesAndNewlines),
           !title.isEmpty else {
         return nil
@@
     }
     let normalized = title.lowercased()
     guard !normalized.isEmpty,
+          normalized.contains("claude"),
           normalized != "claude code",
           !normalized.hasPrefix("claude ·") else {
         return nil
     }
+    // Keep this path to stable identifiers only; volatile status/counter text
+    // should fall back to generic:claude so it stays throttled.
     return "specific:\(normalized)"
 }

As per coding guidelines, title-change handlers must not introduce scalable transcript/directory rescans without a bounded size or benchmark.

🤖 Prompt for 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.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift` around lines 611 -
641, The specificClaudeTitleKey function is creating unique specific keys for
volatile titles that change frequently (spinner characters, elapsed time, token
counts), which bypasses the detectionScanThrottle mechanism. Modify
specificClaudeTitleKey to return nil for titles that contain transient status
indicators or unstable content, so they fall back to "generic:claude" in the
claudeTitleDetectionKey method. Only return specific keys for titles that
represent stable session/project identifiers. This prevents each title update
from being treated as a new specific Claude title and triggering unnecessary
rescans.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift`:
- Around line 537-540: The clearTitleDetectionState call in the stateChanged and
record.state == .ended check is keyed only by surfaceID without verifying if a
newer active session already exists on that surface, causing stale ended records
to incorrectly cancel newer provisional sessions. Before calling
clearTitleDetectionState with the surfaceID, add a check to confirm there is no
current non-ended live session for that surface ID, and only proceed with the
cleanup if that condition is met. This ensures stale session cleanup respects
active surface occupants that may have been created after the old session ended.
- Around line 444-455: The claimedDetectedTranscriptSessionIDs collection is
marking resolved.sessionID as claimed on line 445 before verifying whether the
transcript will actually be bound to the session. If the guard condition on the
next line returns early due to an existing transcript path, the sessionID
remains incorrectly marked as claimed even though the resolved path was never
used. Move the insertion of resolved.sessionID into
claimedDetectedTranscriptSessionIDs to only occur after confirming the resolved
path will be bound to the session, specifically within the registry.update call
block where the transcriptPath is actually being set, or add an additional
condition to only claim when the resolved path matches an existing bound path.
- Around line 344-350: The ClaudeTranscriptResolutionKey used in the freshness
guard does not include workspaceID, which can cause stale cache issues when a
surface is re-adopted or moved to another workspace. Add workspaceID to the
ClaudeTranscriptResolutionKey tuple definition (alongside workingDirectory,
claimedSessionIDs, titleKey, and forceScan) to ensure the deduplication guard
properly handles workspace changes. Additionally, add a guard check to verify
the current live workspace binding matches before applying a cached resolution,
preventing stale workspace bindings from being restored when the surface binding
has changed.
- Around line 611-641: The specificClaudeTitleKey function is creating unique
specific keys for volatile titles that change frequently (spinner characters,
elapsed time, token counts), which bypasses the detectionScanThrottle mechanism.
Modify specificClaudeTitleKey to return nil for titles that contain transient
status indicators or unstable content, so they fall back to "generic:claude" in
the claudeTitleDetectionKey method. Only return specific keys for titles that
represent stable session/project identifiers. This prevents each title update
from being treated as a new specific Claude title and triggering unnecessary
rescans.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ad50ff25-39ca-4915-b7f7-d52dc51be58c

📥 Commits

Reviewing files that changed from the base of the PR and between 3559ddc and 3a807fc.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptService.swift
💤 Files with no reviewable changes (1)
  • Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift

@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Mobile/AgentChat/AgentChatTranscriptService.swift (1)

143-153: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t let a stale transcript-bound detected session suppress later Claude launches.

A hookless title-detected session can stay .idle with transcriptPath after the terminal title moves away from Claude. If the same surface later starts a new hookless Claude, this branch returns before scheduling resolution, so the old transcript remains bound. Please invalidate/end the detected live session on a non-Claude transition, or gate this short-circuit on a still-current title/session generation.

As per coding guidelines, cached session-surface identity needs invalidation or rescan triggers when it can become stale.

🤖 Prompt for 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.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift` around lines 143 -
153, The guard statement that checks bound.transcriptPath == nil allows stale
title-detected sessions to block new Claude launches on the same surface. Either
invalidate the bound session when a non-Claude terminal title transition is
detected (before returning true), or add a validation check before the guard
statement to confirm the session is still current by verifying its title
generation or session generation matches the current state, ensuring stale
transcript-bound sessions do not suppress new Claude launches.

Source: Coding guidelines

🤖 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/Mobile/AgentChat/AgentChatTranscriptService`+TitleDetection.swift:
- Around line 169-190: The claimedDetectedTranscriptSessionIDs.insert call is
happening before confirming that the transcript will actually be bound to a
session, which means if the guard bound.transcriptPath == nil check fails and we
return early, the sessionID is already marked as claimed and will be excluded
from future scans even though it was never actually bound. Move the insert
statement to occur only after successful binding: add
claimedDetectedTranscriptSessionIDs.insert(resolved.sessionID) after the
registry.update call completes within the if let bound block, and also add it
after the registry.adoptDetectedSession call to handle both the update and
adoption scenarios.

---

Outside diff comments:
In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift`:
- Around line 143-153: The guard statement that checks bound.transcriptPath ==
nil allows stale title-detected sessions to block new Claude launches on the
same surface. Either invalidate the bound session when a non-Claude terminal
title transition is detected (before returning true), or add a validation check
before the guard statement to confirm the session is still current by verifying
its title generation or session generation matches the current state, ensuring
stale transcript-bound sessions do not suppress new Claude launches.
🪄 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: dc3ead02-7426-4ad8-aecb-d0049fc774d5

📥 Commits

Reviewing files that changed from the base of the PR and between 3559ddc and 85166eb.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (4)
  • Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptService+TitleDetection.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptService.swift
  • cmux.xcodeproj/project.pbxproj
💤 Files with no reviewable changes (1)
  • Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift

@greptile-apps

greptile-apps Bot commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the main-thread beachball caused by Claude terminal title churn. It debounces per-surface title-change adoption through the existing burst coalescer, moves transcript discovery off the main actor into a cancellable detached utility task, and isolates sidebar row selection state into each row so unchanged rows stay behind TabItemView.equatable().

  • Title adoption (AgentChatTranscriptService + TitleDetection): title changes are coalesced per surface (250 ms), cached by delivered key, and adoption is routed surface-first through TerminalController.adoptDetectedAgentSession(titleChange:) instead of a workspace-wide sweep. Transcript scanning runs in a Task.detached(priority: .utility) and hops back to @MainActor only to apply a still-current result; forced retries are bounded per surface (maxTranscriptResolutionForcedRetries = 3).
  • Session registry (AgentChatSessionRegistry): updateLiveSessionIndex now tracks the newest live session incrementally, calling rebuildLiveSessionIndex (O(N) scan) only when a session actually moves off a surface — so the common pre/postToolUse hook path remains O(1).
  • Sidebar (ContentView / TabItemView): isActive is no longer a prop; each row subscribes to tabManager.selectedTabIdPublisher and keeps a local @State var observedIsActive, so unchanged rows pass the equatable check and their body is skipped on selection changes.

Confidence Score: 5/5

Safe to merge — changes are scoped to the title-change adoption path and sidebar row rendering, with no new data-loss or correctness regressions found.

The transcript-resolution path correctly guards stale results via key equality before applying, forced retries are capped at 3 per surface, and the registry's live-session index is updated incrementally so the hot pre/postToolUse hook path avoids any O(N) scan. The sidebar selection fix uses a well-established row-local publisher projection and the equatable contract remains consistent with the new state model.

No files require special attention. The previously noted internal-visibility widening across the AgentChatTranscriptService split is the main design tradeoff but is inherent to Swift's cross-file extension model and was acknowledged in prior review rounds.

Important Files Changed

Filename Overview
Sources/Mobile/AgentChat/AgentChatTranscriptService+TitleDetection.swift New file implementing per-surface title-change coalescing, throttled transcript resolution scheduling, forced-retry capping (max 3), and claimed-session tracking; logic is sound and well-guarded.
Sources/Mobile/AgentChat/AgentChatTranscriptService.swift Core service refactored: title-detection state fields widened to internal for the cross-file extension, adoption handler updated to surface-scoped signature; per-surface state lifecycle correctly tied to session-end via handleRecordChange.
Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift updateLiveSessionIndex now incremental: rebuildLiveSessionIndex (O(N) scan) called only when a session moves off a surface, keeping the hot pre/postToolUse hook path O(1). liveSession dead-PID eviction added to the loop.
Sources/TerminalController+MobileChat.swift New surface-scoped adoptDetectedAgentSession(titleChange:) entry point avoids workspace-wide sweeps on title churn; adoptDetectedAgentSessions refactored to share the same private helper.
Sources/ContentView.swift TabItemView now holds row-local observedIsActive via selectedTabIdPublisher subscription; isActive removed from props and the Equatable == operator, so unchanged rows skip body on selection changes.
Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift Marked Sendable to allow safe capture in Task.detached; no functional changes.
Sources/AppDelegate.swift Single call-site updated to pass the surface-scoped adoptDetectedAgentSession(titleChange:) handler.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GTC as GhosttyTitleChange
    participant ATS as AgentChatTranscriptService (MainActor)
    participant BC as BurstCoalescer (250ms)
    participant TC as TerminalController (MainActor)
    participant DT as Task.detached (utility)
    participant MA as MainActor (apply result)

    GTC->>ATS: scheduleTitleDetectedAdoption(change)
    ATS->>ATS: check pendingTitleChanges and deliveredTitleKeys
    ATS->>BC: signal flushTitleDetectedAdoptions
    Note over BC: coalesces bursts for 250ms
    BC->>ATS: flushTitleDetectedAdoptions
    ATS->>TC: titleAdoptionHandler(pending.change)
    TC->>ATS: adoptDetectedClaudeSession
    ATS->>ATS: scheduleClaudeTranscriptResolution
    ATS->>DT: Task.detached utility scan
    Note over DT: filesystem scan off main actor
    DT-->>MA: await scanTask.value
    MA->>ATS: applyClaudeTranscriptResolution
    ATS->>ATS: guard key still matches
    ATS->>ATS: registry.update or adoptDetectedSession
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GTC as GhosttyTitleChange
    participant ATS as AgentChatTranscriptService (MainActor)
    participant BC as BurstCoalescer (250ms)
    participant TC as TerminalController (MainActor)
    participant DT as Task.detached (utility)
    participant MA as MainActor (apply result)

    GTC->>ATS: scheduleTitleDetectedAdoption(change)
    ATS->>ATS: check pendingTitleChanges and deliveredTitleKeys
    ATS->>BC: signal flushTitleDetectedAdoptions
    Note over BC: coalesces bursts for 250ms
    BC->>ATS: flushTitleDetectedAdoptions
    ATS->>TC: titleAdoptionHandler(pending.change)
    TC->>ATS: adoptDetectedClaudeSession
    ATS->>ATS: scheduleClaudeTranscriptResolution
    ATS->>DT: Task.detached utility scan
    Note over DT: filesystem scan off main actor
    DT-->>MA: await scanTask.value
    MA->>ATS: applyClaudeTranscriptResolution
    ATS->>ATS: guard key still matches
    ATS->>ATS: registry.update or adoptDetectedSession
Loading

Reviews (3): Last reviewed commit: "fix: retain detected transcript claims w..." | Re-trigger Greptile

Comment on lines +280 to +289
private func refreshLiveSessionIndex(surfaceID: String?) {
guard let surfaceID else { return }
if let newest = records.values
.filter({ $0.surfaceID == surfaceID && $0.state != .ended })
.max(by: { $0.lastActivityAt < $1.lastActivityAt }) {
liveSessionIDBySurfaceID[surfaceID] = newest.sessionID
} else {
liveSessionIDBySurfaceID.removeValue(forKey: surfaceID)
}
}

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 O(N) scan over all records on every write — now on the hook-event socket path

refreshLiveSessionIndex filters and reduces over records.values (all sessions, including historical ended ones) to find the newest live session for a surface. It is called twice per update() and noteHookEvent() (once for previous.surfaceID, once for record.surfaceID). Before this PR, noteHookEvent had no per-event scan overhead; now every pre/postToolUse hook event during a Claude tool storm pays O(N) twice. Sessions are never removed from records, so N grows over the app's lifetime. The cache is valuable — but maintaining it via a full scan on every mutation, rather than an incremental update (e.g., only comparing the mutated record against the current cached winner), adds work on the write-hot path to avoid it on the read path. A tighter maintenance strategy — re-evaluate only when previous.surfaceID == record.surfaceID and the mutated record is the cached one or newer — would keep the O(1) read without the per-hook scan.

Comment on lines +144 to +166
guard !claimedDetectedTranscriptSessionIDs.contains(resolved.sessionID) else {
scheduleClaudeTranscriptResolution(
workspaceID: workspaceID,
workingDirectory: workingDirectory,
surfaceID: surfaceID,
excludingSessionID: registry.liveSession(surfaceID: surfaceID)?.sessionID,
titleHint: titleHint,
forceScan: true
)
return
}
if let claimed = registry.record(sessionID: resolved.sessionID),
claimed.surfaceID != nil,
claimed.surfaceID != surfaceID {
scheduleClaudeTranscriptResolution(
workspaceID: workspaceID,
workingDirectory: workingDirectory,
surfaceID: surfaceID,
excludingSessionID: registry.liveSession(surfaceID: surfaceID)?.sessionID,
titleHint: titleHint,
forceScan: true
)
return

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 forceScan: true retries bypass the throttle without a per-surface retry cap

When applyClaudeTranscriptResolution finds that the resolved transcript is already in claimedDetectedTranscriptSessionIDs or bound to a different surface, it re-invokes scheduleClaudeTranscriptResolution with forceScan: true. forceScan: true bypasses the 4-second detectionScanThrottle check entirely. The key equality guard (transcriptResolutionKeys[surfaceID]) and the growing claimed set bound the total retries to the number of claimable transcripts in the working directory, but two surfaces simultaneously racing to resolve transcripts from the same dense project directory (e.g., a shared monorepo) can trigger several back-to-back forced scans before settling. There is no ceiling on how many forced rescans a single applyClaudeTranscriptResolution chain can spawn for a surface. Adding a per-surface forced-retry counter (reset on clearTitleDetectionState) and capping it at, say, 3 would prevent pathological multi-surface collision storms from generating unbounded disk I/O.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Mobile/AgentChat/AgentChatTranscriptService.swift (1)

447-452: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep Claude Code chrome titles on the generic key.

Line 452 makes titles like Claude Code · … / Claude Code (…) into unique specific: keys because line 449 only excludes claude ·. Those status/chrome titles then use forceScan: true in the unresolved bound path, bypassing the 4s throttle and rescanning the scalable .claude/projects/<cwd> transcript collection per churned key. Treat claude code status prefixes as generic instead.

Proposed fix
         let normalized = title.lowercased()
+        let isClaudeCodeChrome = normalized == "claude code"
+            || normalized.hasPrefix("claude code ·")
+            || normalized.hasPrefix("claude code (")
+            || normalized.hasPrefix("claude code -")
+            || normalized.hasPrefix("claude code —")
         guard !normalized.isEmpty,
-              normalized != "claude code",
+              !isClaudeCodeChrome,
               !normalized.hasPrefix("claude ·") else {
             return nil
         }

As per coding guidelines, production event-driven paths over scalable user data should avoid repeated scans and prefer bounded/coalesced work; this path can rescan transcript files for every churned title key.

🤖 Prompt for 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.

In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift` around lines 447 -
452, The guard statement in this function needs to treat Claude Code chrome
titles as generic instead of assigning them unique specific keys. Currently, the
check on line 449 only excludes the exact match "claude code", and the prefix
check on line 450 only excludes "claude ·". This causes titles like "Claude Code
· something" or "Claude Code (something)" to fall through and receive unique
"specific:" keys, bypassing throttling and causing repeated scans. Update the
guard statement to also check that the normalized title does not start with
"claude code " (with a space after "code") as a prefix, treating all Claude Code
status titles as generic keys that should return nil.

Source: Coding guidelines

🤖 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/Mobile/AgentChat/AgentChatSessionRegistry.swift`:
- Around line 75-77: After ending a dead PID's session in the liveSession
method, the code returns a replacement record from the index without verifying
that the replacement's process is alive. If multiple records exist for the same
surface with dead processes, a dead session can be reported as live. After
retrieving the replacement record via
liveSessionIDBySurfaceID[surfaceID].flatMap, recursively call
liveSession(surfaceID:) instead of directly returning the record to ensure the
replacement is also validated and any subsequent dead processes are properly
handled.

---

Outside diff comments:
In `@Sources/Mobile/AgentChat/AgentChatTranscriptService.swift`:
- Around line 447-452: The guard statement in this function needs to treat
Claude Code chrome titles as generic instead of assigning them unique specific
keys. Currently, the check on line 449 only excludes the exact match "claude
code", and the prefix check on line 450 only excludes "claude ·". This causes
titles like "Claude Code · something" or "Claude Code (something)" to fall
through and receive unique "specific:" keys, bypassing throttling and causing
repeated scans. Update the guard statement to also check that the normalized
title does not start with "claude code " (with a space after "code") as a
prefix, treating all Claude Code status titles as generic keys that should
return nil.
🪄 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: 8936c72f-feb3-49a5-9dbf-821a80b52b42

📥 Commits

Reviewing files that changed from the base of the PR and between 85166eb and 2f71fe6.

📒 Files selected for processing (4)
  • Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptResolver.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptService+TitleDetection.swift
  • Sources/Mobile/AgentChat/AgentChatTranscriptService.swift

Comment thread Sources/Mobile/AgentChat/AgentChatSessionRegistry.swift Outdated
@austinywang
austinywang merged commit 5ea81da into main Jun 20, 2026
27 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — e9062424 Deployed Jun 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant