Selaa lähdekoodia

Fix AI Companion per-account meeting history.

Scope previous meetings to the signed-in Google account, backfill legacy test recordings to mqlmac1@gmail.com, and hide history UI when not connected.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 1 kuukausi sitten
vanhempi
commit
59b76f4891
1 muutettua tiedostoa jossa 130 lisäystä ja 4 poistoa
  1. 130 4
      meetings_app/ViewController.swift

+ 130 - 4
meetings_app/ViewController.swift

@@ -279,6 +279,9 @@ final class ViewController: NSViewController {
279 279
         let startedAt: Date
280 280
         let endedAt: Date
281 281
         let audioFilePath: String
282
+        /// Google account email that was signed in when this recording was saved.
283
+        /// Used to scope AI Companion history per-account.
284
+        var accountEmail: String?
282 285
         var microphoneAudioFilePath: String?
283 286
         var systemAudioFilePath: String?
284 287
         var transcriptStatusRaw: String?
@@ -1562,7 +1565,20 @@ private extension ViewController {
1562 1565
         }
1563 1566
         let decoder = JSONDecoder()
1564 1567
         if let decoded = try? decoder.decode([MeetingRecordingSummary].self, from: raw) {
1565
-            aiCompanionLocalRecordings = decoded.sorted(by: { $0.endedAt > $1.endedAt })
1568
+            var migrated = decoded
1569
+            let testEmail = "mqlmac1@gmail.com"
1570
+            var didMigrate = false
1571
+            for idx in migrated.indices {
1572
+                let existing = migrated[idx].accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
1573
+                if existing?.isEmpty != false {
1574
+                    migrated[idx].accountEmail = testEmail
1575
+                    didMigrate = true
1576
+                }
1577
+            }
1578
+            aiCompanionLocalRecordings = migrated.sorted(by: { $0.endedAt > $1.endedAt })
1579
+            if didMigrate {
1580
+                persistAiCompanionLocalRecordings()
1581
+            }
1566 1582
         } else {
1567 1583
             aiCompanionLocalRecordings = []
1568 1584
         }
@@ -1574,6 +1590,18 @@ private extension ViewController {
1574 1590
         UserDefaults.standard.set(encoded, forKey: aiCompanionLocalRecordingsDefaultsKey)
1575 1591
     }
1576 1592
 
1593
+    private func aiCompanionActiveAccountEmail() -> String? {
1594
+        guard hasGoogleSessionAvailable() else { return nil }
1595
+        let raw = scheduleCurrentProfile?.email.trimmingCharacters(in: .whitespacesAndNewlines)
1596
+        guard let raw, raw.isEmpty == false, raw.contains("@") else { return nil }
1597
+        return raw.lowercased()
1598
+    }
1599
+
1600
+    private func aiCompanionRecordingsForActiveAccount() -> [MeetingRecordingSummary] {
1601
+        guard let email = aiCompanionActiveAccountEmail() else { return [] }
1602
+        return aiCompanionLocalRecordings.filter { ($0.accountEmail ?? "").lowercased() == email }
1603
+    }
1604
+
1577 1605
     private func requestSpeechRecognitionAuthorizationIfNeeded() async throws {
1578 1606
         switch SFSpeechRecognizer.authorizationStatus() {
1579 1607
         case .authorized:
@@ -1921,6 +1949,24 @@ private extension ViewController {
1921 1949
                 microphoneURL: session.microphoneAudioFileURL,
1922 1950
                 recordingID: session.id
1923 1951
             )
1952
+            var accountEmail = await MainActor.run { self.aiCompanionActiveAccountEmail() }
1953
+            if accountEmail == nil, self.hasGoogleSessionAvailable() {
1954
+                do {
1955
+                    let token = try await self.googleOAuth.validAccessToken(presentingWindow: self.view.window)
1956
+                    if let profile = try? await self.googleOAuth.fetchUserProfile(accessToken: token) {
1957
+                        let display = await MainActor.run { self.makeGoogleProfileDisplay(from: profile) }
1958
+                        await MainActor.run { [weak self] in
1959
+                            self?.applyGoogleProfile(display)
1960
+                        }
1961
+                        let cleaned = profile.email?.trimmingCharacters(in: .whitespacesAndNewlines)
1962
+                        if let cleaned, cleaned.isEmpty == false {
1963
+                            accountEmail = cleaned.lowercased()
1964
+                        }
1965
+                    }
1966
+                } catch {
1967
+                    // If profile lookup fails, persist recording without account scoping (it will be hidden in AI Companion).
1968
+                }
1969
+            }
1924 1970
             await MainActor.run {
1925 1971
                 let summary = MeetingRecordingSummary(
1926 1972
                     id: session.id,
@@ -1929,6 +1975,7 @@ private extension ViewController {
1929 1975
                     startedAt: session.startedAt,
1930 1976
                     endedAt: Date(),
1931 1977
                     audioFilePath: finalized.mixedURL.path,
1978
+                    accountEmail: accountEmail,
1932 1979
                     microphoneAudioFilePath: finalized.microphoneURL?.path,
1933 1980
                     systemAudioFilePath: finalized.systemURL?.path,
1934 1981
                     transcriptStatusRaw: MeetingTranscriptStatus.notRequested.rawValue,
@@ -2870,15 +2917,92 @@ private extension ViewController {
2870 2917
         titleLabel.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2871 2918
         subtitle.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2872 2919
 
2920
+        let signedIn = hasGoogleSessionAvailable()
2921
+        let activeEmail = aiCompanionActiveAccountEmail()
2922
+
2923
+        if signedIn == false || activeEmail == nil {
2924
+            let locked = roundedContainer(cornerRadius: 14, color: palette.sectionCard)
2925
+            locked.translatesAutoresizingMaskIntoConstraints = false
2926
+            styleSurface(locked, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
2927
+
2928
+            let lockedStack = NSStackView()
2929
+            lockedStack.translatesAutoresizingMaskIntoConstraints = false
2930
+            lockedStack.orientation = .vertical
2931
+            lockedStack.alignment = .leading
2932
+            lockedStack.spacing = 10
2933
+
2934
+            let msg = textLabel(
2935
+                "Connect your Google account to see your AI Companion meeting history.",
2936
+                font: NSFont.systemFont(ofSize: 14, weight: .semibold),
2937
+                color: palette.textPrimary
2938
+            )
2939
+            msg.alignment = .left
2940
+            msg.maximumNumberOfLines = 2
2941
+            msg.lineBreakMode = .byWordWrapping
2942
+
2943
+            lockedStack.addArrangedSubview(msg)
2944
+            locked.addSubview(lockedStack)
2945
+            NSLayoutConstraint.activate([
2946
+                lockedStack.leadingAnchor.constraint(equalTo: locked.leadingAnchor, constant: 14),
2947
+                lockedStack.trailingAnchor.constraint(equalTo: locked.trailingAnchor, constant: -14),
2948
+                lockedStack.topAnchor.constraint(equalTo: locked.topAnchor, constant: 14),
2949
+                lockedStack.bottomAnchor.constraint(equalTo: locked.bottomAnchor, constant: -14)
2950
+            ])
2951
+
2952
+            contentStack.addArrangedSubview(locked)
2953
+            locked.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2954
+
2955
+            // If tokens exist but we haven't fetched the profile yet, try to hydrate it so we can scope history correctly.
2956
+            if signedIn, scheduleCurrentProfile == nil {
2957
+                Task { [weak self] in
2958
+                    guard let self else { return }
2959
+                    do {
2960
+                        let token = try await self.googleOAuth.validAccessToken(presentingWindow: self.view.window)
2961
+                        if let profile = try? await self.googleOAuth.fetchUserProfile(accessToken: token) {
2962
+                            await MainActor.run { [weak self] in
2963
+                                guard let self else { return }
2964
+                                self.applyGoogleProfile(self.makeGoogleProfileDisplay(from: profile))
2965
+                                self.redrawAiCompanionPageIfNeeded()
2966
+                            }
2967
+                        }
2968
+                    } catch {
2969
+                        // Leave UI in connect state; schedule page handles showing auth errors.
2970
+                    }
2971
+                }
2972
+            }
2973
+
2974
+            content.addSubview(contentStack)
2975
+            NSLayoutConstraint.activate([
2976
+                scroll.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
2977
+                scroll.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
2978
+                scroll.topAnchor.constraint(equalTo: panel.topAnchor),
2979
+                scroll.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
2980
+
2981
+                content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
2982
+                content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
2983
+                content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
2984
+                content.bottomAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.bottomAnchor),
2985
+                content.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
2986
+
2987
+                contentStack.leftAnchor.constraint(equalTo: content.leftAnchor, constant: 28),
2988
+                contentStack.rightAnchor.constraint(equalTo: content.rightAnchor, constant: -28),
2989
+                contentStack.topAnchor.constraint(equalTo: content.topAnchor, constant: 16),
2990
+                content.bottomAnchor.constraint(greaterThanOrEqualTo: contentStack.bottomAnchor, constant: 16)
2991
+            ])
2992
+
2993
+            return panel
2994
+        }
2995
+
2873 2996
         if let session = activeMeetingRecordingSession {
2874 2997
             let activeCard = aiCompanionActiveRecordingCard(session: session)
2875 2998
             contentStack.addArrangedSubview(activeCard)
2876 2999
             activeCard.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
2877 3000
         }
2878 3001
 
2879
-        if aiCompanionLocalRecordings.isEmpty {
3002
+        let scopedRecordings = aiCompanionRecordingsForActiveAccount()
3003
+        if scopedRecordings.isEmpty {
2880 3004
             let emptyLabel = textLabel(
2881
-                "No previous meetings yet. End a meeting to save it here and generate notes.",
3005
+                "No previous meetings yet for \(activeEmail ?? "this account"). End a meeting to save it here and generate notes.",
2882 3006
                 font: typography.fieldLabel,
2883 3007
                 color: palette.textMuted
2884 3008
             )
@@ -3059,7 +3183,7 @@ private extension ViewController {
3059 3183
     private func filteredAiCompanionRecordings() -> [MeetingRecordingSummary] {
3060 3184
         let calendar = Calendar.current
3061 3185
         let now = Date()
3062
-        let recordings = aiCompanionLocalRecordings
3186
+        let recordings = aiCompanionRecordingsForActiveAccount()
3063 3187
 
3064 3188
         func previousCalendarWeekBounds(reference: Date) -> (start: Date, end: Date)? {
3065 3189
             guard let thisWeek = calendar.dateInterval(of: .weekOfYear, for: reference),
@@ -9936,6 +10060,8 @@ private extension ViewController {
9936 10060
         scheduleCurrentProfile = profile
9937 10061
 
9938 10062
         updateGoogleAuthButtonTitle()
10063
+        pageCache[.aiCompanion] = nil
10064
+        redrawAiCompanionPageIfNeeded()
9939 10065
 
9940 10066
         guard let profile, let pictureURL = profile.pictureURL else { return }
9941 10067
         let avatarDiameter = scheduleGoogleSignedInAvatarSize