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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- name: Test link filter
run: bash scripts/test-safe-links.sh

- name: Test chat parsing
run: bash scripts/test-chat-parsing.sh

- name: Build
run: |
xcodebuild \
Expand Down
18 changes: 18 additions & 0 deletions NotchBuddy/NotchBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
3756B7E1A55D9CEC8C4D34A4 /* IslandStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8212018F09EA1E96FBD63CD /* IslandStateMachine.swift */; };
3F19756A396F125EC711247F /* IslandTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3CE3617E9D336DDBA10F7A /* IslandTypes.swift */; };
4117E7C8AA231AAE36E7509D /* UploadCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B241B043223AFBBFB7418A5E /* UploadCanvasView.swift */; };
4976F8BF814D6F817370F1D8 /* ChatMarkdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE88A53AEAA637F866D479D /* ChatMarkdownView.swift */; };
4B6AC00F22EC75E90E0157D5 /* AppLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6D7015D6A77AA59323C0E7 /* AppLog.swift */; };
4F1E0B3C838DA3CD6F864BC0 /* CalcomPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3104222B3F17BA5E8DEA5277 /* CalcomPoller.swift */; };
56BE8C733F10A030A638FB60 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1A7D5CBE2AC084A10C5FF3 /* SettingsView.swift */; };
Expand All @@ -42,16 +43,20 @@
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 */; };
6D158183870434FEDF07E050 /* LocalChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8790ACE9F73BD74640A0F20C /* LocalChat.swift */; };
6FC65C190ED614C664C9BC17 /* SafeWebURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FDED98F7F6C755A72C97A3 /* SafeWebURL.swift */; };
733FCC6A45DE963EF2024B4E /* ChatMarkdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE88A53AEAA637F866D479D /* ChatMarkdownView.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 */; };
8FC5907E9506DC79EB1DDC3C /* ChatMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D797CC384B5BC70D34F121BC /* ChatMarkdown.swift */; };
967B525F7D3837F31D740D18 /* ResendPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FDDE03F5F711146937E2 /* ResendPoller.swift */; };
9C653B8291EFFA414565E8A8 /* MusicController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F142FB3952B5F0639383047 /* MusicController.swift */; };
9DD56C95ECABDDCE23652D50 /* HookServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79D8DD79C3DD9BBC584B3CB2 /* HookServer.swift */; };
B2D86BE268D6F4C86BD0C446 /* ChatMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D797CC384B5BC70D34F121BC /* ChatMarkdown.swift */; };
B3EC9CF3C8BD629C3530E8F1 /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = C066E2B991AA4F56B2724263 /* sounds */; };
B605F976F4141435F27CF266 /* IslandRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A1A8206F73376F4CAF2057 /* IslandRootView.swift */; };
C0E18D0F6F5F6D131E6066B8 /* GreetingCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C581F7B1A9F14B281B60063E /* GreetingCanvasView.swift */; };
Expand All @@ -69,6 +74,7 @@
E4B9986674F4D425826F15A8 /* StripePoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF78999D68D56ADE4CEFBE2 /* StripePoller.swift */; };
E776C7D1DE5C62EA8D4BA3E3 /* FileDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3232F258523DD8D09B670363 /* FileDropView.swift */; };
E7A5D23F455565FE1E210398 /* PillCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6A38B5686C4A1F15AB5D29 /* PillCatalog.swift */; };
E81575CE4433C6F88E424A8F /* LocalChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8790ACE9F73BD74640A0F20C /* LocalChat.swift */; };
EB23ED9F1C12EA8B54A76A91 /* ResendPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FDDE03F5F711146937E2 /* ResendPoller.swift */; };
ECECA2F225E82B7C951658D7 /* UploadSequenceEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD65943C32E150F1CD65C31 /* UploadSequenceEngine.swift */; };
F06EE0A97CB77096A337AA8B /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = C066E2B991AA4F56B2724263 /* sounds */; };
Expand All @@ -94,6 +100,7 @@
78BC1357785CA1B099A33969 /* IslandScreenGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandScreenGeometry.swift; sourceTree = "<group>"; };
79D8DD79C3DD9BBC584B3CB2 /* HookServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HookServer.swift; sourceTree = "<group>"; };
7C1A7D5CBE2AC084A10C5FF3 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
8790ACE9F73BD74640A0F20C /* LocalChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalChat.swift; sourceTree = "<group>"; };
8CB8B6E5E9DA6164F979E88F /* GithubPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubPoller.swift; sourceTree = "<group>"; };
8D3CE3617E9D336DDBA10F7A /* IslandTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandTypes.swift; sourceTree = "<group>"; };
9F6D7015D6A77AA59323C0E7 /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = "<group>"; };
Expand All @@ -104,11 +111,13 @@
B2FDED98F7F6C755A72C97A3 /* SafeWebURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeWebURL.swift; sourceTree = "<group>"; };
B89DA1A0C21AE3535A1760BB /* N8nPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = N8nPoller.swift; sourceTree = "<group>"; };
BE03E03D1804B0B92C5C2DAD /* NotionPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotionPoller.swift; sourceTree = "<group>"; };
BFE88A53AEAA637F866D479D /* ChatMarkdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMarkdownView.swift; sourceTree = "<group>"; };
C066E2B991AA4F56B2724263 /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; name = sounds; path = Resources/sounds; sourceTree = SOURCE_ROOT; };
C2E4F18AA940EFF7E047B9F7 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
C581F7B1A9F14B281B60063E /* GreetingCanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GreetingCanvasView.swift; sourceTree = "<group>"; };
D4540F630F8BA04AFFFA3979 /* BotEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BotEngine.swift; sourceTree = "<group>"; };
D710BD7834228045869D0184 /* IslandWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IslandWindowController.swift; sourceTree = "<group>"; };
D797CC384B5BC70D34F121BC /* ChatMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMarkdown.swift; sourceTree = "<group>"; };
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>"; };
Expand All @@ -126,6 +135,8 @@
A29F4C0B02BE91654D02AA0C /* BotCanvasView.swift */,
D4540F630F8BA04AFFFA3979 /* BotEngine.swift */,
3104222B3F17BA5E8DEA5277 /* CalcomPoller.swift */,
D797CC384B5BC70D34F121BC /* ChatMarkdown.swift */,
BFE88A53AEAA637F866D479D /* ChatMarkdownView.swift */,
A83D5410BD030B243ADDCFF4 /* ClaudeService.swift */,
3232F258523DD8D09B670363 /* FileDropView.swift */,
8CB8B6E5E9DA6164F979E88F /* GithubPoller.swift */,
Expand All @@ -137,6 +148,7 @@
8D3CE3617E9D336DDBA10F7A /* IslandTypes.swift */,
FA02B08886939FF6FE0475DD /* IslandViewContent.swift */,
D710BD7834228045869D0184 /* IslandWindowController.swift */,
8790ACE9F73BD74640A0F20C /* LocalChat.swift */,
4F142FB3952B5F0639383047 /* MusicController.swift */,
B89DA1A0C21AE3535A1760BB /* N8nPoller.swift */,
F1BD8F3CAA60CF37640759E1 /* NotchBuddyApp.swift */,
Expand Down Expand Up @@ -300,6 +312,8 @@
CD8DECA66B9E09C97E469DB9 /* BotCanvasView.swift in Sources */,
C24D863967D8D32D828ED4B1 /* BotEngine.swift in Sources */,
4F1E0B3C838DA3CD6F864BC0 /* CalcomPoller.swift in Sources */,
8FC5907E9506DC79EB1DDC3C /* ChatMarkdown.swift in Sources */,
4976F8BF814D6F817370F1D8 /* ChatMarkdownView.swift in Sources */,
2DEB459FBD0BA3CEAAA5FDE9 /* ClaudeService.swift in Sources */,
E776C7D1DE5C62EA8D4BA3E3 /* FileDropView.swift in Sources */,
C808B654C84D9F71DBF8493A /* GithubPoller.swift in Sources */,
Expand All @@ -311,6 +325,7 @@
3F19756A396F125EC711247F /* IslandTypes.swift in Sources */,
065FCE817315454DE3F85F25 /* IslandViewContent.swift in Sources */,
091EC3B2F1060D7EE1317E4E /* IslandWindowController.swift in Sources */,
6D158183870434FEDF07E050 /* LocalChat.swift in Sources */,
5C6D0BC8C546718CAA82BF90 /* MusicController.swift in Sources */,
0A267EE2955C7AD8E02A296C /* N8nPoller.swift in Sources */,
06BF5FD95E03010CAFDBF2D0 /* NotchBuddyApp.swift in Sources */,
Expand Down Expand Up @@ -339,6 +354,8 @@
651FA3F09608DE5D02D61CCE /* BotCanvasView.swift in Sources */,
0E9CE475EC53C67DAB00F8E5 /* BotEngine.swift in Sources */,
1969590AFEE6CB21404346F1 /* CalcomPoller.swift in Sources */,
B2D86BE268D6F4C86BD0C446 /* ChatMarkdown.swift in Sources */,
733FCC6A45DE963EF2024B4E /* ChatMarkdownView.swift in Sources */,
0329E76BA4A8FEEA8A6D76E7 /* ClaudeService.swift in Sources */,
2132477000FD77F7EB234DAA /* FileDropView.swift in Sources */,
C18227D148D99FE6EE3CC51A /* GithubPoller.swift in Sources */,
Expand All @@ -350,6 +367,7 @@
D82E93014C8087AE81511E40 /* IslandTypes.swift in Sources */,
68A950E3C23FD612639D9776 /* IslandViewContent.swift in Sources */,
C61DDE2EC5FDDB39E4AA3B34 /* IslandWindowController.swift in Sources */,
E81575CE4433C6F88E424A8F /* LocalChat.swift in Sources */,
9C653B8291EFFA414565E8A8 /* MusicController.swift in Sources */,
160EDB9D06275E2607DA5151 /* N8nPoller.swift in Sources */,
DE05D09A32117582AC93A24A /* NotchBuddyApp.swift in Sources */,
Expand Down
5 changes: 5 additions & 0 deletions NotchBuddy/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
<true/>
<key>NSAccessibilityUsageDescription</key>
<string>Pour lire le titre de la fenêtre active et l'attacher comme contexte.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSAppleEventsUsageDescription</key>
<string>Pour sauter au terminal, envoyer des mails et piloter Musique.</string>
<key>NSHighResolutionCapable</key>
Expand Down
5 changes: 5 additions & 0 deletions NotchBuddy/Resources/InfoAppStore.plist
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<string>public.app-category.developer-tools</string>
<key>LSUIElement</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
Expand Down
61 changes: 57 additions & 4 deletions NotchBuddy/Sources/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ final class AppState: ObservableObject {
@Published var openAIChatModel: String = ChatProvider.openai.defaultModel {
didSet { UserDefaults.standard.set(openAIChatModel, forKey: "openAIChatModel") }
}
@Published var ollamaChatModel: String = ChatProvider.ollama.defaultModel {
didSet { UserDefaults.standard.set(ollamaChatModel, forKey: "ollamaChatModel") }
}
@Published var lmstudioChatModel: String = ChatProvider.lmstudio.defaultModel {
didSet { UserDefaults.standard.set(lmstudioChatModel, forKey: "lmstudioChatModel") }
}
@Published var ollamaServerURL: String = "" {
didSet { UserDefaults.standard.set(ollamaServerURL, forKey: "ollamaServerURL") }
}
@Published var lmstudioServerURL: String = "" {
didSet { UserDefaults.standard.set(lmstudioServerURL, forKey: "lmstudioServerURL") }
}

// The always-on workspace pill (default: VS Code). Persisted.
@Published var mainPillId: String = PillCatalog.defaultMainPillId {
Expand All @@ -85,6 +97,41 @@ final class AppState: ObservableObject {
func fetchModelsIfNeeded(for provider: ChatProvider) {
guard !loadingProviderModels.contains(provider),
fetchedProviderModels[provider] == nil else { return }
// Local providers: fetch from server URL (no API key needed)
if provider.isLocal {
let baseURL = provider == .ollama ? ollamaServerURL : lmstudioServerURL
let normalised = LocalChat.normaliseURL(baseURL)
guard !normalised.isEmpty else {
providerModelFetchError[provider] = provider == .ollama
? "Connect Ollama in Settings → Chat first."
: "Connect LM Studio in Settings → Chat first."
return
}
loadingProviderModels.insert(provider)
providerModelFetchError.removeValue(forKey: provider)
Task {
let result = await LocalChat.fetchModelsResult(baseURL: normalised)
loadingProviderModels.remove(provider)
switch result {
case .success(let models) where models.isEmpty:
providerModelFetchError[provider] = provider == .ollama
? "No models yet. Download one in Ollama first."
: "No models yet. Download one in LM Studio first."
case .success(let models):
fetchedProviderModels[provider] = models
let current = provider == .ollama ? ollamaChatModel : lmstudioChatModel
if !models.contains(where: { $0.id == current }) {
let first = models.first!.id
if provider == .ollama { ollamaChatModel = first }
else { lmstudioChatModel = first }
}
case .failure:
providerModelFetchError[provider] = "Cannot reach \(normalised). Is the server running?"
}
}
return
}
// Remote providers: require API key
guard let apiKey = KeychainStore.shared.get(provider.keychainKey), !apiKey.isEmpty else {
providerModelFetchError[provider] = "No API key — add it in Settings."
return
Expand All @@ -97,14 +144,13 @@ final class AppState: ObservableObject {
case .anthropic: models = await ClaudeService.fetchModels(apiKey: apiKey)
case .google: models = await ClaudeService.fetchGoogleModels(apiKey: apiKey)
case .openai: models = await ClaudeService.fetchOpenAIModels(apiKey: apiKey)
case .ollama, .lmstudio: models = [] // handled above
}
loadingProviderModels.remove(provider)
if models.isEmpty {
providerModelFetchError[provider] = "Failed to load models. Check your API key."
} else {
fetchedProviderModels[provider] = models
// If the saved model isn't in the fetched list, pick a sensible default:
// prefer "sonnet" (Anthropic), "flash" (Google), "mini" (OpenAI); else first.
switch provider {
case .anthropic:
if !models.contains(where: { $0.id == claudeModel }) {
Expand All @@ -118,6 +164,7 @@ final class AppState: ObservableObject {
if !models.contains(where: { $0.id == openAIChatModel }) {
openAIChatModel = models.first(where: { $0.id.contains("mini") })?.id ?? models.first!.id
}
case .ollama, .lmstudio: break
}
}
}
Expand All @@ -129,6 +176,8 @@ final class AppState: ObservableObject {
case .anthropic: return claudeModel
case .google: return googleChatModel
case .openai: return openAIChatModel
case .ollama: return ollamaChatModel
case .lmstudio: return lmstudioChatModel
}
}

Expand Down Expand Up @@ -256,6 +305,10 @@ final class AppState: ObservableObject {
if let v = ud.string(forKey: "chatProvider"), let p = ChatProvider(rawValue: v) { chatProvider = p }
if let v = ud.string(forKey: "googleChatModel"), !v.isEmpty { googleChatModel = v }
if let v = ud.string(forKey: "openAIChatModel"), !v.isEmpty { openAIChatModel = v }
if let v = ud.string(forKey: "ollamaChatModel"), !v.isEmpty { ollamaChatModel = v }
if let v = ud.string(forKey: "lmstudioChatModel"), !v.isEmpty { lmstudioChatModel = v }
if let v = ud.string(forKey: "ollamaServerURL"), !v.isEmpty { ollamaServerURL = v }
if let v = ud.string(forKey: "lmstudioServerURL"), !v.isEmpty { lmstudioServerURL = v }
// Migrate old 60s default → 15s
if let v = ud.object(forKey: "autoCloseInterval") as? Double {
autoCloseInterval = (v == 60) ? 15 : v
Expand Down Expand Up @@ -569,8 +622,8 @@ struct NotionPage: Identifiable {

enum ChatRole { case user, assistant }

struct ChatMessage: Identifiable {
struct ChatMessage: Identifiable, Equatable {
let id = UUID()
let role: ChatRole
let content: String
var content: String // var for streaming updates
}
Loading
Loading