Repository navigation
Improve Simulator discoverability on macOS and iOS #9888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
24a3c08
Improve Simulator discoverability
azooz2003-bit 445870a
Merge remote-tracking branch 'origin/main' into task-improve-simulato…
azooz2003-bit 901915e
Fix Simulator button accessibility label
azooz2003-bit 290955b
Address simulator discoverability review findings
azooz2003-bit 2cec093
Fix simulator picker getter return
azooz2003-bit 7a84690
Unify simulator stream state ownership
azooz2003-bit 9e77288
Stabilize simulator stream environment lifetime
azooz2003-bit 0efe264
Own mobile terminal runtime before SwiftUI mounting
azooz2003-bit db6f334
Expose borrowed mobile session view
azooz2003-bit 62fc0aa
Allow process-owned terminal runtime retry
azooz2003-bit 4968ed8
Import terminal theme in renderer recovery view
azooz2003-bit 282d6aa
Load iOS Ghostty config from owned path
azooz2003-bit e356c7f
Resolve renderer recovery strings from package
azooz2003-bit 9b65abd
Add renderer recovery preview fixture
azooz2003-bit 24c437f
Stabilize Simulator discoverability UI tests
azooz2003-bit 7144595
Instrument Simulator selection lifecycle
azooz2003-bit ba45abe
Expose Simulator lifecycle diagnostic timeline
azooz2003-bit c2fc815
Fix Simulator discoverability UI assertions
azooz2003-bit 3a60ad2
Avoid quadratic simulator picker snapshots
azooz2003-bit a4fb70f
Test macOS tab bar omits Simulator button
azooz2003-bit 48c2c7c
Remove Simulator from macOS tab defaults
azooz2003-bit 3d83481
Test crowded tab bar keeps default actions visible
azooz2003-bit f7097c9
Remove unrelated crowded tab bar test
azooz2003-bit ee2df25
Test Simulator toolbar returns from active panel
azooz2003-bit 08cc2f7
Add Simulator tab navigation on iOS
azooz2003-bit 9eedc37
Merge remote-tracking branch 'origin/main' into task-improve-simulato…
azooz2003-bit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
8 changes: 8 additions & 0 deletions
8
...s/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+SimulatorPreview.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| #if DEBUG | ||
| extension MobileShellComposite { | ||
| /// Enables the Simulator-stream capability for an isolated debug preview instance. | ||
| public func enableSimulatorStreamPreviewCapability() { | ||
| supportedHostCapabilities.insert(Self.simulatorStreamCapability) | ||
| } | ||
| } | ||
| #endif |
98 changes: 98 additions & 0 deletions
98
...iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SimulatorDiscoverabilityPreviewView.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| import CMUXMobileCore | ||
| import CmuxMobileBrowser | ||
| import CmuxMobileBrowserStream | ||
| import CmuxMobileShell | ||
| import CmuxMobileShellModel | ||
| import SwiftUI | ||
|
|
||
| #if os(iOS) && DEBUG | ||
| /// Deterministic real-workspace fixture for Simulator chrome and stream-state verification. | ||
| public struct SimulatorDiscoverabilityPreviewView: View { | ||
| @State private var store: MobileShellComposite | ||
| @State private var browserStore = BrowserSurfaceStore() | ||
| @State private var browserStreamStore = BrowserStreamStore() | ||
| @State private var simulatorStreamStore: MobileSimulatorStreamStore | ||
|
|
||
| /// Creates a fixture with zero, one, two, or unsupported Simulator panels. | ||
| public init(mode: String, state: String) { | ||
| let workspaceID = "simulator-discoverability-workspace" | ||
| let descriptors = Self.descriptors(mode: mode, state: state, workspaceID: workspaceID) | ||
| let streamStore = MobileSimulatorStreamStore() | ||
| streamStore.replaceSimulatorPanels(in: workspaceID, with: descriptors) | ||
| streamStore.setSimulatorStreamConnectionStatus( | ||
| state == "disconnected" ? .disconnected : .connected | ||
| ) | ||
|
|
||
| if state != "inactive", let first = descriptors.first { | ||
| _ = streamStore.activate(panelID: first.panelID, in: workspaceID) | ||
| if state == "locked" { | ||
| streamStore.state(for: first.panelID)?.markLockedByOtherConnection() | ||
| } else if state == "stalled" { | ||
| streamStore.state(for: first.panelID)?.markStreamStale() | ||
| } | ||
| } | ||
|
|
||
| let terminalID = MobileTerminalPreview.ID(rawValue: "simulator-fixture-terminal") | ||
| let workspace = MobileWorkspacePreview( | ||
| id: MobileWorkspacePreview.ID(rawValue: workspaceID), | ||
| name: "Simulator Fixture", | ||
| terminals: [MobileTerminalPreview(id: terminalID, name: "Terminal")], | ||
| simulators: descriptors | ||
| ) | ||
| let shellStore = MobileShellComposite( | ||
| isSignedIn: true, | ||
| connectionState: .connected, | ||
| connectedHostName: "UI Test Mac", | ||
| workspaces: [workspace], | ||
| simulatorStreamStore: streamStore | ||
| ) | ||
| if mode != "unsupported" { | ||
| shellStore.enableSimulatorStreamPreviewCapability() | ||
| } | ||
| shellStore.selectedWorkspaceID = workspace.id | ||
| shellStore.selectedTerminalID = terminalID | ||
|
|
||
| _store = State(initialValue: shellStore) | ||
| _simulatorStreamStore = State(initialValue: streamStore) | ||
| } | ||
|
|
||
| public var body: some View { | ||
| WorkspaceShellView(store: store, signOut: {}, showAddDevice: nil) | ||
| .environment(browserStore) | ||
| .environment(browserStreamStore) | ||
| .environment(simulatorStreamStore) | ||
| } | ||
|
|
||
| private static func descriptors( | ||
| mode: String, | ||
| state: String, | ||
| workspaceID: String | ||
| ) -> [MobileSimulatorPanelDescriptor] { | ||
| let count: Int | ||
| switch mode { | ||
| case "one": count = 1 | ||
| case "two": count = 2 | ||
| case "unsupported": count = 1 | ||
| default: count = 0 | ||
| } | ||
| return (0..<count).map { index in | ||
| let suffix = index == 0 ? "A" : "B" | ||
| return MobileSimulatorPanelDescriptor( | ||
| panelID: "sim-\(suffix.lowercased())", | ||
| workspaceID: workspaceID, | ||
| title: "Simulator \(suffix)", | ||
| selectedDeviceName: "iPhone \(suffix)", | ||
| selectedDeviceState: "Booted", | ||
| status: "streaming", | ||
| isReady: true, | ||
| supportsTouch: true, | ||
| supportsKeyboard: true, | ||
| supportsHardwareButtons: true, | ||
| supportsRotation: true, | ||
| ownerConnectionID: state == "locked" ? "other-phone" : nil, | ||
| isOwnedByCurrentConnection: state == "locked" ? false : nil | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| #endif |
35 changes: 35 additions & 0 deletions
35
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SimulatorPickerControl.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import CMUXMobileCore | ||
| import CmuxMobileSupport | ||
| import SwiftUI | ||
|
|
||
| /// One-tap workspace-chrome control for discovering an interactive Mac Simulator panel. | ||
| struct SimulatorPickerControl: View { | ||
| let value: SimulatorPickerMenuValue | ||
| let selectPanel: (String) -> Void | ||
| let terminalTheme: TerminalTheme | ||
|
|
||
| var body: some View { | ||
| Button { | ||
| if let targetPanelID = value.targetPanelID { | ||
| selectPanel(targetPanelID) | ||
| } | ||
| } label: { | ||
| Label( | ||
| L10n.string("mobile.simulatorStream.menuTitle", defaultValue: "Mac Simulators"), | ||
| systemImage: value.activePanelID == nil ? "iphone" : "iphone.circle.fill" | ||
| ) | ||
| .labelStyle(.iconOnly) | ||
| } | ||
| .frame(minWidth: 44, minHeight: 44) | ||
| .contentShape(Rectangle()) | ||
| .foregroundStyle(terminalTheme.terminalChromeForegroundColor) | ||
| .accessibilityLabel( | ||
| L10n.string("mobile.simulatorStream.menuTitle", defaultValue: "Mac Simulators") | ||
| ) | ||
| .accessibilityIdentifier("MobileSimulatorPicker") | ||
| .accessibilityValue( | ||
| value.rows.first(where: { $0.id == value.activePanelID })?.label ?? "" | ||
| ) | ||
| .disabled(value.targetPanelID == nil) | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SimulatorPickerMenuValue.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /// Immutable state that controls whether the workspace chrome presents Simulator panels. | ||
| struct SimulatorPickerMenuValue: Equatable { | ||
| let rows: [SimulatorStreamPickerRow] | ||
| let activePanelID: String? | ||
|
|
||
| init( | ||
| supportsSimulatorStream: Bool, | ||
| rows: [SimulatorStreamPickerRow], | ||
| activePanelID: String? | ||
| ) { | ||
| self.rows = supportsSimulatorStream ? rows : [] | ||
| self.activePanelID = rows.contains(where: { $0.id == activePanelID }) | ||
| ? activePanelID | ||
| : nil | ||
| } | ||
|
|
||
| var isVisible: Bool { | ||
| !rows.isEmpty | ||
| } | ||
|
|
||
| /// The current panel wins; otherwise the first authoritative workspace row is deterministic. | ||
| var targetPanelID: String? { | ||
| activePanelID ?? rows.first?.id | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
...es/iOS/CmuxMobileShellUI/Tests/CmuxMobileShellUITests/SimulatorPickerMenuValueTests.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import CMUXMobileCore | ||
| import Testing | ||
| @testable import CmuxMobileShellUI | ||
|
|
||
| @Suite struct SimulatorPickerMenuValueTests { | ||
| @Test func unsupportedOrEmptyWorkspacesHideTheControl() { | ||
| let row = SimulatorStreamPickerRow(descriptor(id: "sim-a")) | ||
|
|
||
| let unsupported = SimulatorPickerMenuValue( | ||
| supportsSimulatorStream: false, | ||
| rows: [row], | ||
| activePanelID: nil | ||
| ) | ||
| let empty = SimulatorPickerMenuValue( | ||
| supportsSimulatorStream: true, | ||
| rows: [], | ||
| activePanelID: nil | ||
| ) | ||
|
|
||
| #expect(!unsupported.isVisible) | ||
| #expect(unsupported.targetPanelID == nil) | ||
| #expect(!empty.isVisible) | ||
| #expect(empty.targetPanelID == nil) | ||
| } | ||
|
|
||
| @Test func onePanelIsTheOneTapTarget() { | ||
| let value = SimulatorPickerMenuValue( | ||
| supportsSimulatorStream: true, | ||
| rows: [SimulatorStreamPickerRow(descriptor(id: "sim-a"))], | ||
| activePanelID: nil | ||
| ) | ||
|
|
||
| #expect(value.isVisible) | ||
| #expect(value.targetPanelID == "sim-a") | ||
| } | ||
|
|
||
| @Test func multiplePanelsPreferTheActivePanelOrDeterministicFirstFallback() { | ||
| let rows = [ | ||
| SimulatorStreamPickerRow(descriptor(id: "sim-a")), | ||
| SimulatorStreamPickerRow(descriptor(id: "sim-b")), | ||
| ] | ||
| let active = SimulatorPickerMenuValue( | ||
| supportsSimulatorStream: true, | ||
| rows: rows, | ||
| activePanelID: "sim-b" | ||
| ) | ||
| let stale = SimulatorPickerMenuValue( | ||
| supportsSimulatorStream: true, | ||
| rows: rows, | ||
| activePanelID: "removed" | ||
| ) | ||
|
|
||
| #expect(active.activePanelID == "sim-b") | ||
| #expect(active.targetPanelID == "sim-b") | ||
| #expect(stale.activePanelID == nil) | ||
| #expect(stale.targetPanelID == "sim-a") | ||
| } | ||
|
|
||
| private func descriptor(id: String) -> MobileSimulatorPanelDescriptor { | ||
| MobileSimulatorPanelDescriptor( | ||
| panelID: id, | ||
| workspaceID: "workspace-1", | ||
| title: "Simulator \(id)", | ||
| selectedDeviceName: "iPhone \(id)", | ||
| selectedDeviceState: "Booted", | ||
| status: "streaming", | ||
| isReady: true, | ||
| supportsTouch: true, | ||
| supportsKeyboard: true, | ||
| supportsHardwareButtons: true, | ||
| supportsRotation: true | ||
| ) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.