Add observation mode views for First Meeting onboarding (step 4)#1356
Conversation
Create three new views for the observation mode flow that appears as the final step in FirstMeetingFlowView: - ObservationModeView: Pitch message referencing the user's first task candidate, duration selector (3/5/10 min), and start/skip buttons - ObservationSessionView: Timer countdown with progress indicator, running narration bubbles (stubbed), and stop-early button - ObservationSummaryView: Post-observation insights with sparkle icons, proposed first autonomous action, and let's-try-it/maybe-later buttons Wire the three views into FirstMeetingFlowView step 4 using an ObservationPhase enum (pitch -> session -> summary) for sub-step transitions. Add observationDurationMinutes and observationInsights properties to OnboardingState. Both completion paths (accept/decline) set observationCompleted = true and call onComplete. Closes #1346 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| for i in 1..<Self.stubNarrations.count { | ||
| DispatchQueue.main.asyncAfter(deadline: .now() + narrationInterval * Double(i) + 2.0) { | ||
| addNextNarration() | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 Scheduled narration callbacks continue to mutate shared state after observation stops
When the user taps "Stop early" or the timer naturally completes, stopObservation() only invalidates the Timer but does not cancel the pre-scheduled DispatchQueue.main.asyncAfter callbacks at lines 170 and 187. These callbacks call addNextNarration() which appends to state.observationInsights (line 205) even after the view has transitioned to ObservationSummaryView.
Root Cause and Impact
In startObservation(), narration messages are pre-scheduled using DispatchQueue.main.asyncAfter at fixed future times:
// Line 170: initial narration at +2s
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
addNextNarration()
}
// Lines 186-190: remaining narrations at intervals
for i in 1..<Self.stubNarrations.count {
DispatchQueue.main.asyncAfter(deadline: .now() + narrationInterval * Double(i) + 2.0) {
addNextNarration()
}
}These dispatched blocks cannot be cancelled. When the user stops early, stopObservation() (line 208) only invalidates the Timer, but the asyncAfter blocks remain queued.
When they fire, addNextNarration() (line 193) appends to state.observationInsights (line 205). Since ObservationSummaryView reads state.observationInsights via its displayInsights computed property (ObservationSummaryView.swift:19-29), the insights list can grow while the summary is already being displayed, potentially causing the displayed insights to change mid-view or after the user has already read them.
For example, with a 5-minute observation (default), narrations are scheduled at ~62s intervals. If the user stops after 10 seconds, up to 4 remaining narration callbacks will fire over the next ~4 minutes, each appending to state.observationInsights.
Impact: Shared onboarding state is mutated after the observation phase has ended, which could cause the summary view's insight list to change unexpectedly.
Prompt for agents
Replace the DispatchQueue.main.asyncAfter scheduling approach with a cancellable mechanism. One approach: add a @State private var isStopped: Bool = false flag, set it to true in stopObservation(), and add a guard !isStopped else { return } check at the top of addNextNarration() (before the existing guard). Alternatively, use DispatchWorkItem instances stored in a @State array that can be cancelled in stopObservation(). The fix should be applied in ObservationSessionView.swift. Both the initial narration dispatch at line 170 and the loop dispatches at lines 186-190 need to be covered.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8eab534b2d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| private func stopObservation() { | ||
| timer?.invalidate() | ||
| timer = nil |
There was a problem hiding this comment.
Cancel queued narration callbacks when stopping observation
stopObservation() only invalidates the repeating timer, but startObservation() also enqueues multiple DispatchQueue.main.asyncAfter callbacks that still call addNextNarration() after the session is stopped. When a user taps “Stop early” (or leaves this screen quickly), those delayed callbacks can continue appending to state.observationInsights, so the summary can change after observation has ended and onboarding state is mutated out of phase with the UI.
Useful? React with 👍 / 👎.
Summary
observationDurationMinutesandobservationInsightsproperties to OnboardingStateCloses #1346
Test plan
🤖 Generated with Claude Code