From c29e837efd8357ad8387bc7a5f8c63ee69fc34f4 Mon Sep 17 00:00:00 2001 From: Louis Raille Date: Fri, 2 Oct 2026 22:29:00 +0200 Subject: [PATCH] Greet the user by their own first name, not a hardcoded one Co-authored-by: TheodoreRiant <57949703+TheodoreRiant@users.noreply.github.com> --- .../NotchBuddy.xcodeproj/project.pbxproj | 6 ++++ NotchBuddy/Sources/App/ClaudeService.swift | 25 ++++++++++---- NotchBuddy/Sources/App/UserIdentity.swift | 33 +++++++++++++++++++ 3 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 NotchBuddy/Sources/App/UserIdentity.swift diff --git a/NotchBuddy/NotchBuddy.xcodeproj/project.pbxproj b/NotchBuddy/NotchBuddy.xcodeproj/project.pbxproj index 71c9eec5e..92d192850 100644 --- a/NotchBuddy/NotchBuddy.xcodeproj/project.pbxproj +++ b/NotchBuddy/NotchBuddy.xcodeproj/project.pbxproj @@ -39,12 +39,14 @@ 61CB7049490F61CFA1B36B64 /* AppLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6D7015D6A77AA59323C0E7 /* AppLog.swift */; }; 61F73B27A7E3AFF584B2CC22 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E4F18AA940EFF7E047B9F7 /* AppState.swift */; }; 62C4EE3F74DAA59764729E62 /* UploadCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B241B043223AFBBFB7418A5E /* UploadCanvasView.swift */; }; + 63F9F3AAC881D3EDDDD8AED9 /* UserIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A13AEE6E06F46CEC9D908C /* UserIdentity.swift */; }; 651FA3F09608DE5D02D61CCE /* BotCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A29F4C0B02BE91654D02AA0C /* BotCanvasView.swift */; }; 68A950E3C23FD612639D9776 /* IslandViewContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA02B08886939FF6FE0475DD /* IslandViewContent.swift */; }; 6FC65C190ED614C664C9BC17 /* SafeWebURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FDED98F7F6C755A72C97A3 /* SafeWebURL.swift */; }; 78B3C6814431B2DD9D1F4949 /* SoundEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE21C2B315D37AA70ED45E9 /* SoundEngine.swift */; }; 78FCEDC8F78B683AB10E6529 /* NotionPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE03E03D1804B0B92C5C2DAD /* NotionPoller.swift */; }; 878924960FC0F2B3524D48AC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 192844E7299DEED5C2372067 /* AppDelegate.swift */; }; + 8A6B3688D6B3BF728D106D8B /* UserIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4A13AEE6E06F46CEC9D908C /* UserIdentity.swift */; }; 8C32A0C3EAF536D3836F61D5 /* GreetingCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C581F7B1A9F14B281B60063E /* GreetingCanvasView.swift */; }; 8D1C01F283FA7806119A1C85 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1A7D5CBE2AC084A10C5FF3 /* SettingsView.swift */; }; 967B525F7D3837F31D740D18 /* ResendPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FDDE03F5F711146937E2 /* ResendPoller.swift */; }; @@ -110,6 +112,7 @@ D8212018F09EA1E96FBD63CD /* IslandStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandStateMachine.swift; sourceTree = ""; }; E91B733ADC638E14E9BB6EFA /* WindowContextCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowContextCapture.swift; sourceTree = ""; }; F1BD8F3CAA60CF37640759E1 /* NotchBuddyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchBuddyApp.swift; sourceTree = ""; }; + F4A13AEE6E06F46CEC9D908C /* UserIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentity.swift; sourceTree = ""; }; FA02B08886939FF6FE0475DD /* IslandViewContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandViewContent.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -146,6 +149,7 @@ 2EF78999D68D56ADE4CEFBE2 /* StripePoller.swift */, B241B043223AFBBFB7418A5E /* UploadCanvasView.swift */, 6FD65943C32E150F1CD65C31 /* UploadSequenceEngine.swift */, + F4A13AEE6E06F46CEC9D908C /* UserIdentity.swift */, 71718D1B87A39A4879DD2A9D /* VercelPoller.swift */, E91B733ADC638E14E9BB6EFA /* WindowContextCapture.swift */, ); @@ -319,6 +323,7 @@ 1558BB00C530B721172F6E65 /* StripePoller.swift in Sources */, 62C4EE3F74DAA59764729E62 /* UploadCanvasView.swift in Sources */, FAD78D74C6906D4EC8AA4101 /* UploadSequenceEngine.swift in Sources */, + 8A6B3688D6B3BF728D106D8B /* UserIdentity.swift in Sources */, 118FC4AE0AC74D84B4A134D8 /* VercelPoller.swift in Sources */, 2875610F701376BD65573386 /* WindowContextCapture.swift in Sources */, ); @@ -357,6 +362,7 @@ E4B9986674F4D425826F15A8 /* StripePoller.swift in Sources */, 4117E7C8AA231AAE36E7509D /* UploadCanvasView.swift in Sources */, ECECA2F225E82B7C951658D7 /* UploadSequenceEngine.swift in Sources */, + 63F9F3AAC881D3EDDDD8AED9 /* UserIdentity.swift in Sources */, 0E83C1DD4D20E7FB7A904450 /* VercelPoller.swift in Sources */, 2CC2E3B688B15BBEB3AE3BCE /* WindowContextCapture.swift in Sources */, ); diff --git a/NotchBuddy/Sources/App/ClaudeService.swift b/NotchBuddy/Sources/App/ClaudeService.swift index 5144fc49c..7336198cd 100644 --- a/NotchBuddy/Sources/App/ClaudeService.swift +++ b/NotchBuddy/Sources/App/ClaudeService.swift @@ -190,12 +190,25 @@ final class ClaudeService { conversationMessages = [] } - private let systemPrompt = """ - You are Mochi, Louis's personal AI assistant embedded in the notch of his Mac. \ - You have web search access and can help with absolutely anything — research, coding, finding places, recommendations, tasks, questions. \ - Respond in the user's language. Be thorough and complete — use as much detail as the task requires. \ - No markdown formatting (no **, no ##, no bullet dashes). Use plain text with line breaks. - """ + /// Resolved once: NSFullUserName() is a system call, and the name cannot change under us + /// while the app runs. + private let systemPrompt = ClaudeService.makeSystemPrompt() + + /// Greets the user by their macOS first name when there is one worth using, and stays + /// neutral otherwise — same wording as the Windows build. + private nonisolated static func makeSystemPrompt() -> String { + let opening = if let firstName = resolveUserFirstName() { + "You are Mochi, \(firstName)'s personal AI assistant embedded in the notch of their Mac." + } else { + "You are Mochi, a personal AI assistant embedded in the notch of the user's Mac." + } + return """ + \(opening) \ + You have web search access and can help with absolutely anything — research, coding, finding places, recommendations, tasks, questions. \ + Respond in the user's language. Be thorough and complete — use as much detail as the task requires. \ + No markdown formatting (no **, no ##, no bullet dashes). Use plain text with line breaks. + """ + } private let webSearchTools: [[String: Any]] = [ ["type": "web_search_20250305", "name": "web_search", "max_uses": 5] diff --git a/NotchBuddy/Sources/App/UserIdentity.swift b/NotchBuddy/Sources/App/UserIdentity.swift new file mode 100644 index 000000000..7b2bf4565 --- /dev/null +++ b/NotchBuddy/Sources/App/UserIdentity.swift @@ -0,0 +1,33 @@ +import Foundation + +/// First name of the macOS account holder, or nil when the account name is not something +/// you would greet someone by. "Théodore Riant" gives "Théodore"; a login handle like +/// "theodoreriant" or "t.riant2" gives nil, because "Hey theodoreriant!" reads worse than +/// a greeting with no name at all. +func resolveUserFirstName() -> String? { + let fullName = NSFullUserName().trimmingCharacters(in: .whitespacesAndNewlines) + guard !fullName.isEmpty, fullName.count <= maxUserFullNameLength else { return nil } + guard !looksLikeLoginHandle(fullName) else { return nil } + + guard let first = fullName.split(separator: " ").first.map(String.init), + !first.isEmpty, + first.allSatisfy(isNamePart) else { return nil } + return first +} + +/// NSFullUserName() falls back to the short account name when the full name is unset, so a +/// single lowercase word, or one carrying digits or separators, is treated as a handle. +private func looksLikeLoginHandle(_ name: String) -> Bool { + guard !name.contains(" ") else { return false } + return name == name.lowercased() || name.contains(where: isHandleMarker) +} + +private func isHandleMarker(_ character: Character) -> Bool { + character.isNumber || "._-@".contains(character) +} + +private func isNamePart(_ character: Character) -> Bool { + character.isLetter || character == "'" || character == "-" +} + +private let maxUserFullNameLength = 32