mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
fix(ios): lock iPhone to portrait + move chat-start preflight off MainActor
Two iOS-specific crash classes from the v2.5.1 TestFlight feedback round: **Rotation crash** — locked the iPhone target to `UIInterfaceOrientationPortrait` only (was Portrait + LandscapeLeft + LandscapeRight). The phone can't rotate the app at all anymore, so any layout path that wasn't audited for size-class transitions is no longer reachable. iPad orientation list left alone (target device family is iPhone-only anyway). **"Crash while typing" / "trying to continue an existing conversation"** — `ChatController.passModelPreflight()` was doing a synchronous SSH read (`context.readText(configYAML)`) on `@MainActor` during chat-start. On a remote ScarfGo context that blocks the main thread for seconds; iOS's non-responsive-app watchdog kills the process around 10s. To the user this surfaces as a "crash" while they're typing — they kept tapping the keyboard while the connect was hung. Move the read to `Task.detached` and await it; the UI stays responsive while the SSH I/O drains. Three callers (`start`, `start(projectPath:)`, `startResuming`) updated to `await passModelPreflight(...)` — they were already async. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1041,10 +1041,21 @@ final class ChatController {
|
|||||||
/// the start intent so the preflight sheet can replay it after the
|
/// the start intent so the preflight sheet can replay it after the
|
||||||
/// user picks a model. Reads via `context.readText` (transport-
|
/// user picks a model. Reads via `context.readText` (transport-
|
||||||
/// aware) and parses with the ScarfCore YAML parser — same path
|
/// aware) and parses with the ScarfCore YAML parser — same path
|
||||||
/// `IOSSettingsViewModel.load` uses, just synchronous because the
|
/// `IOSSettingsViewModel.load` uses.
|
||||||
/// preflight runs before any `state = .connecting` UI transition.
|
///
|
||||||
private func passModelPreflight(intent: PendingStart) -> Bool {
|
/// **Off MainActor.** `context.readText` synchronously calls
|
||||||
let raw = context.readText(context.paths.configYAML) ?? ""
|
/// `transport.fileExists` + `transport.readFile`; on a remote
|
||||||
|
/// ScarfGo context that's a blocking SSH round-trip that, before
|
||||||
|
/// this fix, ran on the controller's `@MainActor` and stalled the
|
||||||
|
/// UI for seconds during connect — long enough for iOS's
|
||||||
|
/// non-responsive-app watchdog to kill the process if the user
|
||||||
|
/// kept tapping (the typing TestFlight crash report). Reading
|
||||||
|
/// detached pushes the I/O off MainActor; the result and the
|
||||||
|
/// `pendingStartIntent` / `modelPreflightReason` writes hop back.
|
||||||
|
private func passModelPreflight(intent: PendingStart) async -> Bool {
|
||||||
|
let path = context.paths.configYAML
|
||||||
|
let ctx = context
|
||||||
|
let raw = await Task.detached { ctx.readText(path) ?? "" }.value
|
||||||
let config = HermesConfig(yaml: raw)
|
let config = HermesConfig(yaml: raw)
|
||||||
let result = ModelPreflight.check(config)
|
let result = ModelPreflight.check(config)
|
||||||
if result.isConfigured { return true }
|
if result.isConfigured { return true }
|
||||||
@@ -1138,7 +1149,7 @@ final class ChatController {
|
|||||||
/// can type and hit send immediately.
|
/// can type and hit send immediately.
|
||||||
func start() async {
|
func start() async {
|
||||||
if state == .connecting || state == .ready { return }
|
if state == .connecting || state == .ready { return }
|
||||||
guard passModelPreflight(intent: .fresh) else { return }
|
guard await passModelPreflight(intent: .fresh) else { return }
|
||||||
state = .connecting
|
state = .connecting
|
||||||
vm.reset()
|
vm.reset()
|
||||||
let client = ACPClient.forIOSApp(
|
let client = ACPClient.forIOSApp(
|
||||||
@@ -1651,7 +1662,7 @@ final class ChatController {
|
|||||||
} else {
|
} else {
|
||||||
intent = .fresh
|
intent = .fresh
|
||||||
}
|
}
|
||||||
guard passModelPreflight(intent: intent) else { return }
|
guard await passModelPreflight(intent: intent) else { return }
|
||||||
state = .connecting
|
state = .connecting
|
||||||
let client = ACPClient.forIOSApp(
|
let client = ACPClient.forIOSApp(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -1735,7 +1746,7 @@ final class ChatController {
|
|||||||
/// to `session/load` if the remote doesn't support `session/resume`
|
/// to `session/load` if the remote doesn't support `session/resume`
|
||||||
/// (Hermes < 0.9.x).
|
/// (Hermes < 0.9.x).
|
||||||
func startResuming(sessionID: String) async {
|
func startResuming(sessionID: String) async {
|
||||||
guard passModelPreflight(intent: .resume(sessionID: sessionID)) else { return }
|
guard await passModelPreflight(intent: .resume(sessionID: sessionID)) else { return }
|
||||||
await stop()
|
await stop()
|
||||||
vm.reset()
|
vm.reset()
|
||||||
// Clear eagerly so a lingering project name from a prior
|
// Clear eagerly so a lingering project name from a prior
|
||||||
|
|||||||
@@ -540,7 +540,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -582,7 +582,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|||||||
Reference in New Issue
Block a user