Skip to content

Commit 5c068e8

Browse files
Pyinerclaude
andcommitted
Merge TASK-1077 iOS home scroll observation-boundary root fix
Cut the home view tree's observation dependency on the god model: independent root navigation store, home view no longer holds the whole model, rows take value objects + action closures. Removes the build105 scroll-freeze patch. Debug probe: model_publish~60 but root/home body~2-3, hitch_time_ratio 0.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 parents f6b4198 + dcf1b59 commit 5c068e8

13 files changed

Lines changed: 423 additions & 328 deletions

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileAutomationViews.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ struct GaryxAutomationThreadPickerRow: View {
10151015
VStack(spacing: 0) {
10161016
GaryxSwipeActionRow(id: "thread:\(thread.id)", actions: threadSwipeActions) {
10171017
GaryxSidebarThreadRowView(
1018-
model: GaryxSidebarThreadRowPresentation(
1018+
presentation: GaryxSidebarThreadRowPresentation(
10191019
thread: thread,
10201020
isSelected: isSelected,
10211021
isPinned: isPinned,

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileModel+Gateway.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ extension GaryxMobileModel {
160160
selectedAutomationEditor = nil
161161
selectedAgentDetail = nil
162162
selectedTeamDetail = nil
163-
selectedRouteNotFound = nil
163+
routeNotFoundStore.selection = nil
164164
isLoadingThreads = false
165165
remoteStateLoadPhase = .idle
166166
isLoadingSelectedThreadHistory = false

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileModel+Navigation.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,13 @@ extension GaryxMobileModel {
344344
selectedAutomationEditor = nil
345345
selectedAgentDetail = nil
346346
selectedTeamDetail = nil
347-
selectedRouteNotFound = nil
347+
routeNotFoundStore.selection = nil
348348
closeSkillDetail()
349349
}
350350

351351
private func showRouteNotFound(kind: String, id: String) {
352352
let target = id.trimmingCharacters(in: .whitespacesAndNewlines)
353-
selectedRouteNotFound = GaryxMobileRouteNotFound(
353+
routeNotFoundStore.selection = GaryxMobileRouteNotFound(
354354
title: "\(kind) Not Found",
355355
message: target.isEmpty
356356
? "Garyx could not find the requested \(kind.lowercased())."

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileModel+Presentation.swift

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,10 @@ import WidgetKit
55

66
extension GaryxMobileModel {
77
func refreshHomeThreadListSnapshot() {
8-
homeThreadListStore.applyUnlessInteracting(
9-
isThreadListInteracting: isThreadListInteracting
10-
) {
11-
homeThreadListInput
12-
}
13-
}
14-
15-
func setThreadListInteracting(_ isInteracting: Bool) {
16-
guard isThreadListInteracting != isInteracting else { return }
17-
isThreadListInteracting = isInteracting
18-
guard !isInteracting else { return }
19-
homeThreadListStore.flushDeferredInteractionRefresh {
20-
homeThreadListInput
21-
}
22-
flushDeferredRecentThreadsWidgetSnapshotPersistence()
23-
}
24-
25-
func endThreadListInteractionIfHomeBecameHidden(previousNavigationState: GaryxMobileNavigationState) {
26-
guard !previousNavigationState.presentsContent, navigationState.presentsContent else { return }
27-
setThreadListInteracting(false)
8+
#if DEBUG
9+
GaryxHomeScrollPerformanceProbe.shared.markHomeListStoreApply()
10+
#endif
11+
homeThreadListStore.apply(homeThreadListInput)
2812
}
2913

3014
var homeThreadListInput: GaryxHomeThreadListInput {
@@ -212,6 +196,65 @@ extension GaryxMobileModel {
212196
return true
213197
}
214198

199+
#if DEBUG
200+
func startHomeScrollPressureProbeIfRequested() {
201+
let environment = ProcessInfo.processInfo.environment
202+
let arguments = CommandLine.arguments
203+
guard environment["GARYX_MOBILE_HOME_SCROLL_PROBE"] == "1"
204+
|| arguments.contains("--garyx-home-scroll-probe")
205+
else { return }
206+
debugSnapshotActive = true
207+
loadHomeScrollPressureFixture()
208+
Task { [weak self] in
209+
try? await Task.sleep(nanoseconds: 500_000_000)
210+
guard let self else { return }
211+
let probe = GaryxHomeScrollPerformanceProbe.shared
212+
probe.beginWindow(label: "home_scroll_60hz_render_snapshot")
213+
let threadId = "thread-0"
214+
for tick in 0..<60 {
215+
guard !Task.isCancelled else { break }
216+
renderSnapshotsByThread[threadId] = GaryxRenderSnapshot(
217+
basedOnSeq: tick,
218+
rows: [],
219+
tailActivity: .thinking,
220+
visibleMessageIds: ["message-\(tick)"]
221+
)
222+
try? await Task.sleep(nanoseconds: 16_666_667)
223+
}
224+
_ = probe.endWindow()
225+
}
226+
}
227+
228+
private func loadHomeScrollPressureFixture() {
229+
let formatter = ISO8601DateFormatter()
230+
formatter.formatOptions = [.withInternetDateTime]
231+
let now = Date()
232+
threads = (0..<50).map { index in
233+
GaryxThreadSummary(
234+
id: "thread-\(index)",
235+
title: "Synthetic thread \(index)",
236+
createdAt: formatter.string(from: now.addingTimeInterval(Double(-index) * 3_600)),
237+
updatedAt: formatter.string(from: now.addingTimeInterval(Double(-index) * 180)),
238+
lastMessagePreview: "Synthetic preview \(index)",
239+
workspacePath: "/Users/test/workspaces/project-\(index % 6)",
240+
messageCount: 10 + index,
241+
agentId: "agent-\(index % 4)",
242+
teamId: nil,
243+
teamName: nil,
244+
providerType: "codex",
245+
recentRunId: "run-\(index)",
246+
activeRunId: index == 0 ? "run-\(index)" : nil,
247+
runState: index == 0 ? "running" : "idle",
248+
worktreePath: nil
249+
)
250+
}
251+
pinnedThreadIds = (0..<6).map { "thread-\($0)" }
252+
recentThreadIds = (0..<50).map { "thread-\($0)" }
253+
connectionState = .ready(version: "debug-home-scroll-probe")
254+
refreshHomeThreadListSnapshot()
255+
}
256+
#endif
257+
215258
func sidebarThreadSummary(for threadId: String) -> GaryxThreadSummary? {
216259
let normalizedId = threadId.trimmingCharacters(in: .whitespacesAndNewlines)
217260
guard !normalizedId.isEmpty else { return nil }

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileModel+Threads.swift

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,6 @@ extension GaryxMobileModel {
269269
}
270270

271271
func persistRecentThreadsWidgetSnapshot() {
272-
guard !isThreadListInteracting else {
273-
hasDeferredRecentThreadsWidgetSnapshotPersistence = true
274-
return
275-
}
276272
var summariesById: [String: GaryxThreadSummary] = [:]
277273
for thread in threads where summariesById[thread.id] == nil {
278274
summariesById[thread.id] = thread
@@ -307,12 +303,6 @@ extension GaryxMobileModel {
307303
WidgetCenter.shared.reloadTimelines(ofKind: GaryxRecentThreadsWidgetConstants.kind)
308304
}
309305

310-
func flushDeferredRecentThreadsWidgetSnapshotPersistence() {
311-
guard hasDeferredRecentThreadsWidgetSnapshotPersistence else { return }
312-
hasDeferredRecentThreadsWidgetSnapshotPersistence = false
313-
persistRecentThreadsWidgetSnapshot()
314-
}
315-
316306
func widgetAgentIdentity(for thread: GaryxThreadSummary) -> WidgetAgentIdentity {
317307
let teamId = thread.teamId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
318308
if !teamId.isEmpty {
@@ -593,12 +583,7 @@ extension GaryxMobileModel {
593583
return
594584
}
595585
let initialCandidateThreadIds = backgroundCommittedRunCandidateThreadIds()
596-
guard GaryxBackgroundThreadReconcilePolicy.shouldRefreshThreads(
597-
isThreadListInteracting: isThreadListInteracting,
598-
candidateThreadIds: initialCandidateThreadIds
599-
) else {
600-
return
601-
}
586+
guard initialCandidateThreadIds.contains(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) else { return }
602587
await refreshThreads(silent: true)
603588
guard runtimeGeneration == gatewayRuntimeGeneration else { return }
604589
for threadId in backgroundCommittedRunCandidateThreadIds() {

mobile/garyx-mobile/App/GaryxMobile/GaryxMobileModel.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ struct GaryxMobileRouteNotFound: Identifiable, Equatable {
3333
let message: String
3434
}
3535

36+
@MainActor
37+
final class GaryxRouteNotFoundStore: ObservableObject {
38+
@Published var selection: GaryxMobileRouteNotFound?
39+
}
40+
3641
@MainActor
3742
final class GaryxMobileModel: ObservableObject {
3843
static let threadListPageLimit = 30
@@ -132,7 +137,7 @@ final class GaryxMobileModel: ObservableObject {
132137
}
133138
@Published var navigationState = GaryxMobileNavigationState() {
134139
didSet {
135-
endThreadListInteractionIfHomeBecameHidden(previousNavigationState: oldValue)
140+
rootNavigationPathStore.apply(navigationState: navigationState)
136141
refreshHomeThreadListSnapshot()
137142
}
138143
}
@@ -215,7 +220,6 @@ final class GaryxMobileModel: ObservableObject {
215220
@Published var selectedAutomationEditor: GaryxAutomationSummary?
216221
@Published var selectedAgentDetail: GaryxAgentSummary?
217222
@Published var selectedTeamDetail: GaryxTeamSummary?
218-
@Published var selectedRouteNotFound: GaryxMobileRouteNotFound?
219223
var skillEditorLoadRequestId: UUID?
220224
var skillFileLoadRequestId: UUID?
221225
@Published var draftThreadTitle = ""
@@ -291,9 +295,9 @@ final class GaryxMobileModel: ObservableObject {
291295
var workspaceRefreshRequestId: UUID?
292296
var nextThreadListOffset = 0
293297
var lastPersistedWidgetThreads: [GaryxMobileWidgetThread]?
294-
var hasDeferredRecentThreadsWidgetSnapshotPersistence = false
298+
let rootNavigationPathStore = GaryxRootNavigationPathStore()
299+
let routeNotFoundStore = GaryxRouteNotFoundStore()
295300
let homeThreadListStore = GaryxHomeThreadListStore()
296-
var isThreadListInteracting = false
297301
var hasAttemptedLastOpenedThreadRestore = false
298302
var selectedThreadNextHistoryBeforeIndex: Int?
299303
var sceneRefreshTask: Task<Void, Never>?
@@ -345,6 +349,11 @@ final class GaryxMobileModel: ObservableObject {
345349
)
346350
}
347351
#endif
352+
rootNavigationPathStore.apply(navigationState: navigationState)
348353
refreshHomeThreadListSnapshot()
354+
#if DEBUG
355+
GaryxHomeScrollPerformanceProbe.shared.attachModelObjectWillChange(objectWillChange)
356+
startHomeScrollPressureProbeIfRequested()
357+
#endif
349358
}
350359
}

0 commit comments

Comments
 (0)