Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NotchBuddy/NotchBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -110,6 +112,7 @@
D8212018F09EA1E96FBD63CD /* IslandStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandStateMachine.swift; sourceTree = "<group>"; };
E91B733ADC638E14E9BB6EFA /* WindowContextCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowContextCapture.swift; sourceTree = "<group>"; };
F1BD8F3CAA60CF37640759E1 /* NotchBuddyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchBuddyApp.swift; sourceTree = "<group>"; };
F4A13AEE6E06F46CEC9D908C /* UserIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIdentity.swift; sourceTree = "<group>"; };
FA02B08886939FF6FE0475DD /* IslandViewContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandViewContent.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -146,6 +149,7 @@
2EF78999D68D56ADE4CEFBE2 /* StripePoller.swift */,
B241B043223AFBBFB7418A5E /* UploadCanvasView.swift */,
6FD65943C32E150F1CD65C31 /* UploadSequenceEngine.swift */,
F4A13AEE6E06F46CEC9D908C /* UserIdentity.swift */,
71718D1B87A39A4879DD2A9D /* VercelPoller.swift */,
E91B733ADC638E14E9BB6EFA /* WindowContextCapture.swift */,
);
Expand Down Expand Up @@ -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 */,
);
Expand Down Expand Up @@ -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 */,
);
Expand Down
25 changes: 19 additions & 6 deletions NotchBuddy/Sources/App/ClaudeService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
33 changes: 33 additions & 0 deletions NotchBuddy/Sources/App/UserIdentity.swift
Original file line number Diff line number Diff line change
@@ -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
Loading