Skip to content

Reduce title-driven workspace list fanout - #6564

Closed
Eridanus117 wants to merge 1 commit into
manaflow-ai:mainfrom
Eridanus117:codex/title-fanout-core
Closed

Eridanus117 wants to merge 1 commit into
manaflow-ai:mainfrom
Eridanus117:codex/title-fanout-core

Conversation

@Eridanus117

@Eridanus117 Eridanus117 commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a default-on terminal.titleUpdates.workspaceListFanout.enabled setting so existing title behavior stays unchanged.
  • When disabled, automatic terminal title updates still update panel/process state, but no longer mutate Workspace.title.
  • Teach the mobile workspace-list observer to stop subscribing to automatic panelTitles changes and exclude automatic panel titles from its hash while still tracking manual terminal renames.

Root cause

Automatic terminal title updates can fan out through the workspace-list path: a runtime title update mutates Workspace.panelTitles, the focused/single-panel path mutates Workspace.title, and the mobile workspace-list observer hashes/subscribes to those automatic title changes.

This PR targets that fanout path directly. The setting defaults to true, so default behavior is unchanged. Users who set it to false trade live automatic runtime-title freshness in the workspace/mobile list for lower title-driven list churn.

Relationship to other PRs

This PR is now independent from #6552 and is based on main.

Behavior

With terminal.titleUpdates.workspaceListFanout.enabled = false:

  • Workspace.panelTitles still records the automatic terminal runtime title.
  • Process title state still gets refreshed.
  • Workspace.title remains stable for automatic runtime-title churn.
  • Mobile workspace-list hashing ignores automatic panel title churn.
  • Manual terminal renames still affect the mobile workspace-list hash.

Validation

  • git diff --check
  • python3 scripts/swift_file_length_budget.py --repo-root . --budget .github/swift-file-length-budget.tsv
  • DEVELOPER_DIR=/Applications/Xcode-26.5.0.app/Contents/Developer swift test --package-path Packages/macOS/CmuxSettings --filter UserDefaultsSettingsClientTests
  • DEVELOPER_DIR=/Applications/Xcode-26.5.0.app/Contents/Developer CMUX_ZIG=/opt/homebrew/opt/zig@0.15/bin/zig PATH="/opt/homebrew/opt/zig@0.15/bin:$PATH" xcodebuild -project cmux.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -only-testing:cmuxTests/TabManagerTitleWorkspaceListFanoutTests -only-testing:cmuxTests/MobileWorkspaceListFidelityTests test

Summary by CodeRabbit

  • New Features

    • Added a new setting that controls whether automatic terminal titles are included in workspace list updates, providing granular control over workspace title display behavior.
  • Tests

    • Expanded test coverage for workspace list title propagation behavior, including setting validation and workspace observer hash calculations.

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a feature-flagged setting terminal.titleUpdates.workspaceListFanout.enabled (default true) that gates whether automatic panel titles propagate into the workspace list. Workspace methods gain an updatesWorkspaceTitle parameter; TabManager reads the flag to conditionally pass it; MobileWorkspaceListObserver conditionally subscribes to $panelTitles and adjusts its summaryHash computation accordingly.

Changes

Panel Title Workspace List Fanout Feature Flag

Layer / File(s) Summary
Settings key and fanout settings type
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift, Sources/PanelTitleWorkspaceListFanoutSettings.swift, Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsClientTests.swift
Adds titleUpdateWorkspaceListFanoutEnabled DefaultsKey<Bool> to TerminalCatalogSection and a new PanelTitleWorkspaceListFanoutSettings.isEnabled(settings:) helper. Tests validate the default value (true) and a round-trip write/read of false via UserDefaultsSettingsClient.
Workspace and TabManager fanout gating
Sources/Workspace.swift, Sources/TabManager.swift
applyProcessTitle and updatePanelTitle on Workspace gain an updatesWorkspaceTitle flag (default true) that prevents workspace-title synchronization when false. TabManager reads PanelTitleWorkspaceListFanoutSettings.isEnabled and passes the resulting flag to both call sites.
MobileWorkspaceListObserver conditional subscription and hash
Sources/Mobile/MobileWorkspaceListObserver.swift
Injects SettingsReading into MobileWorkspaceListObserver. $panelTitles is only subscribed when fanout is enabled. summaryHash gains includesAutomaticPanelTitles parameter and delegates per-workspace title resolution to a new workspaceListPanelTitle helper. DEBUG summaryHashForTesting extended accordingly.
Xcode project registration
cmux.xcodeproj/project.pbxproj
Registers PanelTitleWorkspaceListFanoutSettings.swift in the cmux target and TabManagerTitleWorkspaceListFanoutTests.swift in the cmuxTests target across all required PBX sections.
Observer fidelity and TabManager integration tests
cmuxTests/MobileWorkspaceListFidelityTests.swift, cmuxTests/TabManagerTitleWorkspaceListFanoutTests.swift
Adds a fidelity test verifying hash discrimination between automatic and manual panel titles. Adds an integration test that disables fanout, fires .ghosttyDidSetTitle, and asserts that panel/session titles update while workspace title and customTitle remain unchanged.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    Note over TabManager,Workspace: Panel title received from terminal
  end
  participant TabManager
  participant PanelTitleWorkspaceListFanoutSettings
  participant UserDefaultsSettingsClient
  participant Workspace
  participant MobileWorkspaceListObserver

  TabManager->>PanelTitleWorkspaceListFanoutSettings: isEnabled(settings:)
  PanelTitleWorkspaceListFanoutSettings->>UserDefaultsSettingsClient: value(for: titleUpdateWorkspaceListFanoutEnabled)
  UserDefaultsSettingsClient-->>PanelTitleWorkspaceListFanoutSettings: Bool
  PanelTitleWorkspaceListFanoutSettings-->>TabManager: updatesWorkspaceTitle: Bool

  TabManager->>Workspace: updatePanelTitle(panelId:title:updatesWorkspaceTitle:)
  alt updatesWorkspaceTitle == true
    Workspace-->>Workspace: self.title = panelTitle
  else updatesWorkspaceTitle == false
    Workspace-->>Workspace: skip self.title update
  end

  rect rgba(255, 228, 181, 0.5)
    Note over MobileWorkspaceListObserver: Hash computation at emit time
  end
  MobileWorkspaceListObserver->>MobileWorkspaceListObserver: summaryHash(includesAutomaticPanelTitles:)
  MobileWorkspaceListObserver->>MobileWorkspaceListObserver: workspaceListPanelTitle(workspace:panelId:includesAutomaticPanelTitles:)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#5446: Modifies MobileWorkspaceListObserver.summaryHash and per-panel title derivation via panelTitle(panelId:) and $panelCustomTitles, which are the same mechanisms this PR extends with the includesAutomaticPanelTitles flag.

Suggested reviewers

  • lawrencecchen

🐇 A flag was planted, a bool set to true,
Panel titles now fan out — or quietly stay put!
The hash knows which titles to count or to shun,
Custom names still shine bright under any set sun.
Hop on, the workspace list knows what you've done! 🌿


Important

Pre-merge checks failed

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

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error The PR violates swift-architectural-rethink.md by wiring the same behavior through multiple surfaces: MobileWorkspaceListObserver reads the setting inconsistently at subscription setup (line 203) v... Capture includesAutomaticPanelTitlesInWorkspaceList as a stored property at init time rather than a computed property, or subscribe to setting changes to refresh subscriptions when it toggles.
Cmux No Test Or Debug Seam In Production Source ❌ Error New file Sources/Mobile/MobileWorkspaceListObserver.swift adds summaryHashForTesting() with both ForTesting suffix and #if DEBUG guard, used only by tests—a test-debug seam that violates the rule. Remove #if DEBUG guard and ForTesting name from summaryHashForTesting; make it a regular static function or isolate it in a dedicated debug file/folder per #6452.
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Reduce title-driven workspace list fanout' directly and clearly summarizes the main objective of the PR, which is to add a setting to reduce performance churn from automatic title updates cascading through the workspace list.
Description check ✅ Passed The PR description covers the summary, root cause, behavior changes, and validation steps comprehensively. However, it lacks the 'Testing' section specifying manual verification steps, demo video link, and does not follow the repository's description template structure fully.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Production code changes follow Swift 6 actor isolation guidelines. New settings key is a value type; new helper enum is nonisolated; new @MainActor methods have compatible signatures; new settings...
Cmux Swift Blocking Runtime ✅ Passed Production code adds no blocking patterns: new setting helper, computed property reads, and conditional flag passing only. Test file includes deterministic XCTest-based polling scaffolding (allowed...
Cmux Expensive Synchronous Load ✅ Passed PR adds only lightweight UserDefaults reads and conditional control flow to main actor paths; no RestorableAgentSessionIndex.load(), JSON parsing, FileManager ops, or expensive operations introduced.
Cmux Cache Substitution Correctness ✅ Passed No cache substitution issue found. The setting is read fresh from UserDefaults, not cached. The summaryHash is used only for transient change detection, never persisted. Mobile clients always fetch...
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift and Xcode project files; the rule excludes Swift code, which is covered by a separate check (swift-blocking-runtime.md).
Cmux Algorithmic Complexity ✅ Passed No algorithmic complexity violations introduced. PR adds O(1) settings lookups and parameter passing on existing code paths without creating new nested loops, rescans, sorting, or in-memory joins....
Cmux Swift Concurrency ✅ Passed Production code adds only settings checks and conditional flags; no new DispatchQueue, Combine app state, completion-handlers, or fire-and-forget Tasks. Test code appropriately uses XCTest boundaries.
Cmux Swift @Concurrent ✅ Passed All Swift changes are synchronous with no new async functions, @concurrent annotations, or UI-heavy operations without proper actor hops. All work is intentionally UI-bound on @MainActor.
Cmux Swift File And Package Boundaries ✅ Passed All changes comply with swift-file-package-boundaries rules: new helper file is 7 lines (single responsibility), modifications to budgeted files are minimal (+1-2 lines), MobileWorkspaceListObserve...
Cmux Swiftpm Lockfiles ✅ Passed PR includes all required SwiftPM lockfiles: 12 package-local Package.resolved files (all with substantial content), root cmux.xcodeproj Package.resolved (3600 bytes), and no .gitignore files ignore...
Cmux Swift Logging ✅ Passed No logging violations detected. PR adds no print/debugPrint/dump/NSLog in production code; existing NSLog calls are pre-existing and #if DEBUG-guarded; no secrets exposed.
Cmux User-Facing Error Privacy ✅ Passed PR adds only internal configuration keys ("terminal.titleUpdates.workspaceListFanout.enabled"), debug logging, and test data—no user-facing errors, alerts, or sensitive information exposed.
Cmux Full Internationalization ✅ Passed PR contains only backend/infrastructure changes (configuration flag, method signatures, unit tests) with no new user-facing UI text, alerts, dialogs, metadata, web UI, API responses, or modificatio...
Cmux Swiftui State Layout ✅ Passed PR introduces no new ObservableObject, @Published, @StateObject, or @Observable state; no GeometryReader, LazyVStack/LazyHStack store references, or render-time mutations. New helper is non-View ut...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR contains no user-visible window/panel/controller creation or material changes. Changes are limited to settings, observer subscription logic, title update routing, and tests.
Cmux Source Artifacts ✅ Passed All 9 changed files are hand-written source, tests, or build configuration files intentionally part of the product. No artifacts like build output, caches, temp folders, or local tool output were a...
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@Eridanus117
Eridanus117 force-pushed the codex/title-fanout-core branch from 11dc5a3 to 31d0959 Compare June 22, 2026 04:54
@Eridanus117
Eridanus117 marked this pull request as ready for review June 22, 2026 04:55
@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a terminal.titleUpdates.workspaceListFanout.enabled UserDefaults setting (default true) that suppresses the path by which automatic terminal runtime titles mutate Workspace.title and trigger mobile workspace-list refreshes. With the setting disabled, panelTitles and processTitle still update, but Workspace.title is pinned and MobileWorkspaceListObserver neither subscribes to $panelTitles nor includes automatic panel titles in its summary hash.

  • Workspace.applyProcessTitle and updatePanelTitle each gain an updatesWorkspaceTitle: Bool = true parameter; TabManager reads the setting once per notification and threads it through both calls.
  • MobileWorkspaceListObserver conditionally appends the $panelTitles publisher to the per-workspace subscription list and dispatches to a new workspaceListPanelTitle helper that, when automatic titles are excluded, falls back to panelCustomTitles then Panel.displayTitle.
  • New TabManagerTitleWorkspaceListFanoutTests verifies the end-to-end disabled path; MobileWorkspaceListFidelityTests gains a hash-exclusion/re-inclusion test for the manual-rename case.

Confidence Score: 3/5

The fanout-disabled path works correctly end-to-end for the default case, but the subscription system and hash computation diverge when the setting is toggled from disabled back to enabled at runtime without a process restart.

The core logic in Workspace and TabManager is clean and the false → mobile-list-quiesce direction works correctly. The issue is in refreshPerWorkspaceSubscriptions: it captures the setting once at workspace-subscription time, so existing multi-panel workspaces built while the setting was false never acquire a $panelTitles subscription. After the setting flips to true, emitIfNeeded starts hashing automatic titles again, but those workspaces produce no wakeup signal for automatic title changes — the mobile list silently goes stale for them. Because workspace.title is not mutated for multi-panel panels, even the $title fallback does not rescue the case.

Sources/Mobile/MobileWorkspaceListObserver.swift — specifically the refreshPerWorkspaceSubscriptions guard that skips re-subscription for already-tracked workspaces when the setting changes.

Important Files Changed

Filename Overview
Sources/Mobile/MobileWorkspaceListObserver.swift Adds setting-gated $panelTitles subscription and includesAutomaticPanelTitles hash parameter; subscription setup evaluates the setting once at workspace-subscribe time, creating a stale-subscription bug when the setting is re-enabled at runtime for multi-panel workspaces.
Sources/PanelTitleWorkspaceListFanoutSettings.swift New production helper that reads the fanout setting; allocates a fresh SettingCatalog() on every call, which fires on every terminal title update notification.
Sources/TabManager.swift Routes updatesWorkspaceTitle flag (from settings) into updatePanelTitle and applyProcessTitle on title-update paths; changes are minimal and correct.
Sources/Workspace.swift Adds updatesWorkspaceTitle: Bool = true parameter to applyProcessTitle and updatePanelTitle; guard placement correctly prevents workspace.title mutation while still updating processTitle and panelTitles.
cmuxTests/TabManagerTitleWorkspaceListFanoutTests.swift New @mainactor XCTest class with a DispatchQueue.main.asyncAfter-based polling helper; exercises the end-to-end fanout-disabled path correctly, but the polling mechanism relies on XCTWaiter run-loop spinning.
cmuxTests/MobileWorkspaceListFidelityTests.swift Adds automaticTerminalTitleCanBeExcludedFromObserverHash test covering the hash exclusion path and manual-rename re-inclusion; well-structured and correctly exercises both sides of the flag.
Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift Adds titleUpdateWorkspaceListFanoutEnabled DefaultsKey defaulting to true; straightforward settings key addition.
Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsClientTests.swift Adds default-value assertion and round-trip test for the new key; no issues.
cmux.xcodeproj/project.pbxproj Registers PanelTitleWorkspaceListFanoutSettings.swift in the app target and TabManagerTitleWorkspaceListFanoutTests.swift in the test target; no structural issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ghosttyDidSetTitle notification] --> B[TabManager.updatePanelTitle]
    B --> C{Read\nfanout setting}
    C -->|enabled = true| D[tab.updatePanelTitle\nupdatesWorkspaceTitle: true]
    C -->|enabled = false| E[tab.updatePanelTitle\nupdatesWorkspaceTitle: false]
    D --> F[panelTitles updated]
    E --> F
    F --> G{Single panel\n& no customTitle?}
    G -->|enabled = true| H[workspace.title updated\n→ $title fires]
    G -->|enabled = false\nor multi-panel| I[workspace.title unchanged]
    H --> J[MobileWorkspaceListObserver\nemitIfNeeded]
    I --> K{$panelTitles\nsubscribed?}
    K -->|Subscription built while\nenabled=true| L[emitIfNeeded triggered]
    K -->|Subscription built while\nenabled=false| M[⚠️ No wakeup for\nmulti-panel workspaces]
    L --> J
    J --> N{summaryHash\nchanged?}
    N -->|yes| O[emit workspace.updated\nto mobile clients]
    N -->|no| P[suppressed by hash diff]
    style M fill:#ffcccc,stroke:#cc0000
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"}}}%%
flowchart TD
    A[ghosttyDidSetTitle notification] --> B[TabManager.updatePanelTitle]
    B --> C{Read\nfanout setting}
    C -->|enabled = true| D[tab.updatePanelTitle\nupdatesWorkspaceTitle: true]
    C -->|enabled = false| E[tab.updatePanelTitle\nupdatesWorkspaceTitle: false]
    D --> F[panelTitles updated]
    E --> F
    F --> G{Single panel\n& no customTitle?}
    G -->|enabled = true| H[workspace.title updated\n→ $title fires]
    G -->|enabled = false\nor multi-panel| I[workspace.title unchanged]
    H --> J[MobileWorkspaceListObserver\nemitIfNeeded]
    I --> K{$panelTitles\nsubscribed?}
    K -->|Subscription built while\nenabled=true| L[emitIfNeeded triggered]
    K -->|Subscription built while\nenabled=false| M[⚠️ No wakeup for\nmulti-panel workspaces]
    L --> J
    J --> N{summaryHash\nchanged?}
    N -->|yes| O[emit workspace.updated\nto mobile clients]
    N -->|no| P[suppressed by hash diff]
    style M fill:#ffcccc,stroke:#cc0000
Loading

Comments Outside Diff (1)

  1. Sources/Mobile/MobileWorkspaceListObserver.swift, line 179-210 (link)

    P1 Subscription/setting divergence on runtime re-enable

    refreshPerWorkspaceSubscriptions evaluates includesAutomaticPanelTitlesInWorkspaceList once per workspace when perWorkspaceCancellables[workspace.id] == nil. If the setting starts false (subscriptions built without $panelTitles) and is later switched to true at runtime, existing subscriptions are never rebuilt — only newly-added workspaces pick up the $panelTitles publisher. Meanwhile, emitIfNeeded now uses true for the hash, expecting automatic title changes to produce a different hash, but no publisher wakes it up for multi-panel workspaces (where workspace.title is not mutated, so $title also stays silent). The mobile list silently goes stale for automatic title churn on all pre-existing multi-panel workspaces until a process restart.

    The true → false direction is harmless (over-subscribed but hash diffing absorbs the false wakeups), so the asymmetry only bites on re-enable.

Reviews (1): Last reviewed commit: "Control title workspace-list fanout" | Re-trigger Greptile

Comment on lines +1 to +7
import CmuxSettings

enum PanelTitleWorkspaceListFanoutSettings {
static func isEnabled(settings: any SettingsReading) -> Bool {
settings.value(for: SettingCatalog().terminal.titleUpdateWorkspaceListFanoutEnabled)
}
}

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 SettingCatalog() constructed on every title-update call

isEnabled allocates a fresh SettingCatalog() on every invocation. This is called in TabManager.updatePanelTitle and TabManager.updateWorkspaceTitle — both of which fire on every incoming Ghostty ghosttyDidSetTitle notification, which can arrive many times per second during heavy shell activity. While individual DefaultsKey allocations are cheap, constructing the full catalog just to touch one key on a hot notification path is unnecessary overhead. Storing the key directly (e.g. SettingCatalog().terminal.titleUpdateWorkspaceListFanoutEnabled captured once at call-site construction time, or caching the resolved Bool per observer) would avoid the repeated allocation.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +10 to +46
@discardableResult
private func waitForTitleFanoutCondition(
timeout: TimeInterval = 3.0,
pollInterval: TimeInterval = 0.05,
file: StaticString = #filePath,
line: UInt = #line,
_ condition: @escaping () -> Bool
) -> Bool {
if condition() {
return true
}

let expectation = XCTestExpectation(description: "wait for title fanout condition")
let deadline = Date().addingTimeInterval(timeout)

func poll() {
if condition() {
expectation.fulfill()
return
}
guard Date() < deadline else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + pollInterval) {
poll()
}
}

DispatchQueue.main.async {
poll()
}

let result = XCTWaiter().wait(for: [expectation], timeout: timeout + pollInterval + 0.1)
if result != .completed {
XCTFail("Timed out waiting for condition", file: file, line: line)
return false
}
return true
}

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 Polling loop using asyncAfter on @MainActor test

waitForTitleFanoutCondition relies on DispatchQueue.main.asyncAfter scheduling follow-up polls while the calling test (marked @MainActor) blocks on XCTWaiter().wait(...). This works today because XCTWaiter spins the run loop while waiting, draining queued main-thread work. That run-loop-spinning behavior is an implementation detail of the XCTest runtime and is not guaranteed under Swift Testing or if the test runner changes its wait strategy. An AsyncStream/continuation-based approach driven by the same @Published sources under test would give a real-signal-based wait without relying on run-loop spin, and would integrate naturally with Swift concurrency's structured async/await test style already used in MobileWorkspaceListFidelityTests.

@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: 2

🤖 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 `@cmuxTests/TabManagerTitleWorkspaceListFanoutTests.swift`:
- Around line 48-83: Migrate the test file from XCTest framework to Swift
Testing for consistency with the sibling test file in this PR. Convert the
TabManagerTitleWorkspaceListFanoutTests class to a struct decorated with `@Suite`,
change the testTitleWorkspaceListFanoutCanBeDisabled method to be decorated with
`@Test`, replace XCTUnwrap calls with `#require`, and replace all XCTAssertTrue,
XCTAssertEqual, and XCTAssertNil assertions with `#expect` calls. Update the
imports to include the Testing framework and ensure the `@MainActor` decorator is
retained on the struct.

In `@Sources/Mobile/MobileWorkspaceListObserver.swift`:
- Around line 30-48: Convert the computed property
includesAutomaticPanelTitlesInWorkspaceList from a computed property that reads
the setting on every access to a stored property that captures the value once at
initialization time. In the init method, assign the result of
PanelTitleWorkspaceListFanoutSettings.isEnabled(settings: settings) to this
property so the setting value is captured once when the observer is created
rather than being re-read on every access.
🪄 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: d194dded-6529-46df-8bd0-ab4041828266

📥 Commits

Reviewing files that changed from the base of the PR and between 0df6f71 and 31d0959.

📒 Files selected for processing (9)
  • Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/TerminalCatalogSection.swift
  • Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsClientTests.swift
  • Sources/Mobile/MobileWorkspaceListObserver.swift
  • Sources/PanelTitleWorkspaceListFanoutSettings.swift
  • Sources/TabManager.swift
  • Sources/Workspace.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MobileWorkspaceListFidelityTests.swift
  • cmuxTests/TabManagerTitleWorkspaceListFanoutTests.swift

Comment on lines +48 to +83
@MainActor
final class TabManagerTitleWorkspaceListFanoutTests: XCTestCase {
func testTitleWorkspaceListFanoutCanBeDisabled() throws {
let suiteName = "cmux-title-workspace-list-fanout-\(UUID().uuidString)"
let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName))
defaults.removePersistentDomain(forName: suiteName)
defer { defaults.removePersistentDomain(forName: suiteName) }

let settings = UserDefaultsSettingsClient(defaults: defaults)
settings.set(false, for: SettingCatalog().terminal.titleUpdateWorkspaceListFanoutEnabled)

let manager = TabManager(settings: settings)
let workspace = try XCTUnwrap(manager.selectedWorkspace)
let panelId = try XCTUnwrap(workspace.focusedPanelId)
let originalTitle = workspace.title

NotificationCenter.default.post(
name: .ghosttyDidSetTitle,
object: nil,
userInfo: [
GhosttyNotificationKey.tabId: workspace.id,
GhosttyNotificationKey.surfaceId: panelId,
GhosttyNotificationKey.title: "Runtime title"
]
)

XCTAssertTrue(
waitForTitleFanoutCondition(timeout: 1.0) {
workspace.panelTitles[panelId] == "Runtime title" &&
workspace.sessionSnapshot(includeScrollback: false).processTitle == "Runtime title"
}
)
XCTAssertEqual(workspace.title, originalTitle)
XCTAssertNil(workspace.customTitle)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider using Swift Testing for consistency.

The test logic is correct and properly validates the fanout disable behavior. However, the sibling test file MobileWorkspaceListFidelityTests.swift (also part of this PR) uses Swift Testing (@Test, #expect), while this test uses XCTest.

For consistency and modern testing practices, consider migrating this test to Swift Testing:

import Testing
import CmuxSettings

`#if` canImport(cmux_DEV)
`@testable` import cmux_DEV
`#elseif` canImport(cmux)
`@testable` import cmux
`#endif`

`@MainActor`
`@Suite`(.serialized)
struct TabManagerTitleWorkspaceListFanoutTests {
    `@Test` func titleWorkspaceListFanoutCanBeDisabled() throws {
        let suiteName = "cmux-title-workspace-list-fanout-\(UUID().uuidString)"
        let defaults = try `#require`(UserDefaults(suiteName: suiteName))
        defaults.removePersistentDomain(forName: suiteName)
        defer { defaults.removePersistentDomain(forName: suiteName) }

        let settings = UserDefaultsSettingsClient(defaults: defaults)
        settings.set(false, for: SettingCatalog().terminal.titleUpdateWorkspaceListFanoutEnabled)

        let manager = TabManager(settings: settings)
        let workspace = try `#require`(manager.selectedWorkspace)
        let panelId = try `#require`(workspace.focusedPanelId)
        let originalTitle = workspace.title

        NotificationCenter.default.post(
            name: .ghosttyDidSetTitle,
            object: nil,
            userInfo: [
                GhosttyNotificationKey.tabId: workspace.id,
                GhosttyNotificationKey.surfaceId: panelId,
                GhosttyNotificationKey.title: "Runtime title"
            ]
        )

        `#expect`(
            waitForTitleFanoutCondition(timeout: 1.0) {
                workspace.panelTitles[panelId] == "Runtime title" &&
                    workspace.sessionSnapshot(includeScrollback: false).processTitle == "Runtime title"
            }
        )
        `#expect`(workspace.title == originalTitle)
        `#expect`(workspace.customTitle == nil)
    }
}

Note: The waitForTitleFanoutCondition helper would need adjustment to return Bool and use #expect for the timeout failure case, or you could use Swift Testing's built-in async test support if the underlying notification/update mechanism can be awaited.

🧰 Tools
🪛 SwiftLint (0.63.3)

[Warning] 49-49: Classes should have an explicit deinit method

(required_deinit)

🤖 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 `@cmuxTests/TabManagerTitleWorkspaceListFanoutTests.swift` around lines 48 -
83, Migrate the test file from XCTest framework to Swift Testing for consistency
with the sibling test file in this PR. Convert the
TabManagerTitleWorkspaceListFanoutTests class to a struct decorated with `@Suite`,
change the testTitleWorkspaceListFanoutCanBeDisabled method to be decorated with
`@Test`, replace XCTUnwrap calls with `#require`, and replace all XCTAssertTrue,
XCTAssertEqual, and XCTAssertNil assertions with `#expect` calls. Update the
imports to include the Testing framework and ensure the `@MainActor` decorator is
retained on the struct.

Comment on lines +30 to +48
private let settings: any SettingsReading
/// Throttle window with `latest: true`. First event in a burst emits
/// immediately (iPhone gets the change in milliseconds), subsequent
/// events within the window collapse to one trailing emit carrying the
/// final state. So a single action is instant; a burst caps at ~1 emit
/// per 80 ms. Hash-diff suppresses no-op rebroadcasts.
private let throttleMilliseconds: Int = 80
private var includesAutomaticPanelTitlesInWorkspaceList: Bool {
PanelTitleWorkspaceListFanoutSettings.isEnabled(settings: settings)
}

init(tabManager: TabManager, notificationStore: TerminalNotificationStore? = nil) {
init(
tabManager: TabManager,
notificationStore: TerminalNotificationStore? = nil,
settings: any SettingsReading = UserDefaultsSettingsClient(defaults: .standard)
) {
self.tabManager = tabManager
self.notificationStore = notificationStore
self.settings = settings

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider capturing the setting value at initialization time.

The computed property includesAutomaticPanelTitlesInWorkspaceList reads the setting on every access. If the setting changes at runtime while the workspace set remains stable, refreshPerWorkspaceSubscriptions won't re-run, so:

  • Changing from true→false leaves the observer subscribed to $panelTitles (unnecessary but harmless).
  • Changing from false→true means the observer won't receive $panelTitles updates (mobile client misses automatic title changes).

If this setting is intended as launch-time configuration (not a runtime toggle), consider capturing the value once at initialization:

private let includesAutomaticPanelTitlesInWorkspaceList: Bool

init(...) {
    ...
    self.includesAutomaticPanelTitlesInWorkspaceList = 
        PanelTitleWorkspaceListFanoutSettings.isEnabled(settings: settings)
    ...
}

If runtime toggles are intended, subscribe to setting changes and call refreshPerWorkspaceSubscriptions when it changes.

🤖 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/MobileWorkspaceListObserver.swift` around lines 30 - 48,
Convert the computed property includesAutomaticPanelTitlesInWorkspaceList from a
computed property that reads the setting on every access to a stored property
that captures the value once at initialization time. In the init method, assign
the result of PanelTitleWorkspaceListFanoutSettings.isEnabled(settings:
settings) to this property so the setting value is captured once when the
observer is created rather than being re-read on every access.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant