Bläddra i källkod

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 2 månader sedan
förälder
incheckning
59b76f4891
1 ändrade filer med 130 tillägg och 4 borttagningar
  1. 130 4
      meetings_app/ViewController.swift

+ 130 - 4
meetings_app/ViewController.swift

@@ -279,6 +279,9 @@ final class ViewController: NSViewController {
         let startedAt: Date
         let endedAt: Date
         let audioFilePath: String
+        /// Google account email that was signed in when this recording was saved.
+        /// Used to scope AI Companion history per-account.
+        var accountEmail: String?
         var microphoneAudioFilePath: String?
         var systemAudioFilePath: String?
         var transcriptStatusRaw: String?
@@ -1562,7 +1565,20 @@ private extension ViewController {
         }
         let decoder = JSONDecoder()
         if let decoded = try? decoder.decode([MeetingRecordingSummary].self, from: raw) {
-            aiCompanionLocalRecordings = decoded.sorted(by: { $0.endedAt > $1.endedAt })
+            var migrated = decoded
+            let testEmail = "mqlmac1@gmail.com"
+            var didMigrate = false
+            for idx in migrated.indices {
+                let existing = migrated[idx].accountEmail?.trimmingCharacters(in: .whitespacesAndNewlines)
+                if existing?.isEmpty != false {
+                    migrated[idx].accountEmail = testEmail
+                    didMigrate = true
+                }
+            }
+            aiCompanionLocalRecordings = migrated.sorted(by: { $0.endedAt > $1.endedAt })
+            if didMigrate {
+                persistAiCompanionLocalRecordings()
+            }
         } else {
             aiCompanionLocalRecordings = []
         }
@@ -1574,6 +1590,18 @@ private extension ViewController {
         UserDefaults.standard.set(encoded, forKey: aiCompanionLocalRecordingsDefaultsKey)
     }
 
+    private func aiCompanionActiveAccountEmail() -> String? {
+        guard hasGoogleSessionAvailable() else { return nil }
+        let raw = scheduleCurrentProfile?.email.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard let raw, raw.isEmpty == false, raw.contains("@") else { return nil }
+        return raw.lowercased()
+    }
+
+    private func aiCompanionRecordingsForActiveAccount() -> [MeetingRecordingSummary] {
+        guard let email = aiCompanionActiveAccountEmail() else { return [] }
+        return aiCompanionLocalRecordings.filter { ($0.accountEmail ?? "").lowercased() == email }
+    }
+
     private func requestSpeechRecognitionAuthorizationIfNeeded() async throws {
         switch SFSpeechRecognizer.authorizationStatus() {
         case .authorized:
@@ -1921,6 +1949,24 @@ private extension ViewController {
                 microphoneURL: session.microphoneAudioFileURL,
                 recordingID: session.id
             )
+            var accountEmail = await MainActor.run { self.aiCompanionActiveAccountEmail() }
+            if accountEmail == nil, self.hasGoogleSessionAvailable() {
+                do {
+                    let token = try await self.googleOAuth.validAccessToken(presentingWindow: self.view.window)
+                    if let profile = try? await self.googleOAuth.fetchUserProfile(accessToken: token) {
+                        let display = await MainActor.run { self.makeGoogleProfileDisplay(from: profile) }
+                        await MainActor.run { [weak self] in
+                            self?.applyGoogleProfile(display)
+                        }
+                        let cleaned = profile.email?.trimmingCharacters(in: .whitespacesAndNewlines)
+                        if let cleaned, cleaned.isEmpty == false {
+                            accountEmail = cleaned.lowercased()
+                        }
+                    }
+                } catch {
+                    // If profile lookup fails, persist recording without account scoping (it will be hidden in AI Companion).
+                }
+            }
             await MainActor.run {
                 let summary = MeetingRecordingSummary(
                     id: session.id,
@@ -1929,6 +1975,7 @@ private extension ViewController {
                     startedAt: session.startedAt,
                     endedAt: Date(),
                     audioFilePath: finalized.mixedURL.path,
+                    accountEmail: accountEmail,
                     microphoneAudioFilePath: finalized.microphoneURL?.path,
                     systemAudioFilePath: finalized.systemURL?.path,
                     transcriptStatusRaw: MeetingTranscriptStatus.notRequested.rawValue,
@@ -2870,15 +2917,92 @@ private extension ViewController {
         titleLabel.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
         subtitle.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
 
+        let signedIn = hasGoogleSessionAvailable()
+        let activeEmail = aiCompanionActiveAccountEmail()
+
+        if signedIn == false || activeEmail == nil {
+            let locked = roundedContainer(cornerRadius: 14, color: palette.sectionCard)
+            locked.translatesAutoresizingMaskIntoConstraints = false
+            styleSurface(locked, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+
+            let lockedStack = NSStackView()
+            lockedStack.translatesAutoresizingMaskIntoConstraints = false
+            lockedStack.orientation = .vertical
+            lockedStack.alignment = .leading
+            lockedStack.spacing = 10
+
+            let msg = textLabel(
+                "Connect your Google account to see your AI Companion meeting history.",
+                font: NSFont.systemFont(ofSize: 14, weight: .semibold),
+                color: palette.textPrimary
+            )
+            msg.alignment = .left
+            msg.maximumNumberOfLines = 2
+            msg.lineBreakMode = .byWordWrapping
+
+            lockedStack.addArrangedSubview(msg)
+            locked.addSubview(lockedStack)
+            NSLayoutConstraint.activate([
+                lockedStack.leadingAnchor.constraint(equalTo: locked.leadingAnchor, constant: 14),
+                lockedStack.trailingAnchor.constraint(equalTo: locked.trailingAnchor, constant: -14),
+                lockedStack.topAnchor.constraint(equalTo: locked.topAnchor, constant: 14),
+                lockedStack.bottomAnchor.constraint(equalTo: locked.bottomAnchor, constant: -14)
+            ])
+
+            contentStack.addArrangedSubview(locked)
+            locked.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
+
+            // If tokens exist but we haven't fetched the profile yet, try to hydrate it so we can scope history correctly.
+            if signedIn, scheduleCurrentProfile == nil {
+                Task { [weak self] in
+                    guard let self else { return }
+                    do {
+                        let token = try await self.googleOAuth.validAccessToken(presentingWindow: self.view.window)
+                        if let profile = try? await self.googleOAuth.fetchUserProfile(accessToken: token) {
+                            await MainActor.run { [weak self] in
+                                guard let self else { return }
+                                self.applyGoogleProfile(self.makeGoogleProfileDisplay(from: profile))
+                                self.redrawAiCompanionPageIfNeeded()
+                            }
+                        }
+                    } catch {
+                        // Leave UI in connect state; schedule page handles showing auth errors.
+                    }
+                }
+            }
+
+            content.addSubview(contentStack)
+            NSLayoutConstraint.activate([
+                scroll.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
+                scroll.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
+                scroll.topAnchor.constraint(equalTo: panel.topAnchor),
+                scroll.bottomAnchor.constraint(equalTo: panel.bottomAnchor),
+
+                content.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
+                content.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
+                content.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
+                content.bottomAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.bottomAnchor),
+                content.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor),
+
+                contentStack.leftAnchor.constraint(equalTo: content.leftAnchor, constant: 28),
+                contentStack.rightAnchor.constraint(equalTo: content.rightAnchor, constant: -28),
+                contentStack.topAnchor.constraint(equalTo: content.topAnchor, constant: 16),
+                content.bottomAnchor.constraint(greaterThanOrEqualTo: contentStack.bottomAnchor, constant: 16)
+            ])
+
+            return panel
+        }
+
         if let session = activeMeetingRecordingSession {
             let activeCard = aiCompanionActiveRecordingCard(session: session)
             contentStack.addArrangedSubview(activeCard)
             activeCard.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
         }
 
-        if aiCompanionLocalRecordings.isEmpty {
+        let scopedRecordings = aiCompanionRecordingsForActiveAccount()
+        if scopedRecordings.isEmpty {
             let emptyLabel = textLabel(
-                "No previous meetings yet. End a meeting to save it here and generate notes.",
+                "No previous meetings yet for \(activeEmail ?? "this account"). End a meeting to save it here and generate notes.",
                 font: typography.fieldLabel,
                 color: palette.textMuted
             )
@@ -3059,7 +3183,7 @@ private extension ViewController {
     private func filteredAiCompanionRecordings() -> [MeetingRecordingSummary] {
         let calendar = Calendar.current
         let now = Date()
-        let recordings = aiCompanionLocalRecordings
+        let recordings = aiCompanionRecordingsForActiveAccount()
 
         func previousCalendarWeekBounds(reference: Date) -> (start: Date, end: Date)? {
             guard let thisWeek = calendar.dateInterval(of: .weekOfYear, for: reference),
@@ -9936,6 +10060,8 @@ private extension ViewController {
         scheduleCurrentProfile = profile
 
         updateGoogleAuthButtonTitle()
+        pageCache[.aiCompanion] = nil
+        redrawAiCompanionPageIfNeeded()
 
         guard let profile, let pictureURL = profile.pictureURL else { return }
         let avatarDiameter = scheduleGoogleSignedInAvatarSize