diff --git a/TypeaheadAI.xcodeproj/project.pbxproj b/TypeaheadAI.xcodeproj/project.pbxproj
index bc36f65..c27654e 100644
--- a/TypeaheadAI.xcodeproj/project.pbxproj
+++ b/TypeaheadAI.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
2B27450A2AB01CF400F37D3E /* SpecialSaveActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2745092AB01CF400F37D3E /* SpecialSaveActor.swift */; };
2B27450E2AB0380C00F37D3E /* AppContextManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B27450D2AB0380C00F37D3E /* AppContextManager.swift */; };
2B2745102AB03A3D00F37D3E /* CanSimulateCopy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B27450F2AB03A3D00F37D3E /* CanSimulateCopy.swift */; };
+ 2B285D852ACA22FB000C5BDE /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 2B285D842ACA22FB000C5BDE /* LaunchAtLogin */; };
2B2EF14E2AC17D4000EF2BD4 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2EF14D2AC17D4000EF2BD4 /* CustomTextField.swift */; };
2B2EF1502AC40C8F00EF2BD4 /* ChatBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2EF14F2AC40C8F00EF2BD4 /* ChatBubble.swift */; };
2B2EF1522AC40CB500EF2BD4 /* MessagePendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2EF1512AC40CB500EF2BD4 /* MessagePendingView.swift */; };
@@ -55,7 +56,6 @@
2BDA45C32ABEE840006128BC /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BDA45C22ABEE840006128BC /* MessageView.swift */; };
2BE0EC222AA0956C00E47C52 /* ModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0EC212AA0956C00E47C52 /* ModalView.swift */; };
2BE0EC272AA17F9100E47C52 /* MouseClickMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE0EC262AA17F9100E47C52 /* MouseClickMonitor.swift */; };
- 2BF558BE2AB8353B002F2008 /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 2BF558BD2AB8353B002F2008 /* LaunchAtLogin */; };
2BF929792AB04D2600FC105B /* MemoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF929782AB04D2600FC105B /* MemoManager.swift */; };
2BF9297C2AB13EEA00FC105B /* Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = 2BF9297B2AB13EEA00FC105B /* Markdown */; };
2BF929802AB13F3600FC105B /* CodeBlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BF9297F2AB13F3600FC105B /* CodeBlockView.swift */; };
@@ -144,8 +144,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ 2B285D852ACA22FB000C5BDE /* LaunchAtLogin in Frameworks */,
2B4BDB892ACBED2100E55D78 /* SettingsAccess in Frameworks */,
- 2BF558BE2AB8353B002F2008 /* LaunchAtLogin in Frameworks */,
2BA3C2352AADAC5700537F95 /* llama in Frameworks */,
2B473E8C2AA860380042913D /* MenuBarExtraAccess in Frameworks */,
2BF929852AB13FEC00FC105B /* Highlighter in Frameworks */,
@@ -366,7 +366,7 @@
2BA3C2342AADAC5700537F95 /* llama */,
2BF9297B2AB13EEA00FC105B /* Markdown */,
2BF929842AB13FEC00FC105B /* Highlighter */,
- 2BF558BD2AB8353B002F2008 /* LaunchAtLogin */,
+ 2B285D842ACA22FB000C5BDE /* LaunchAtLogin */,
2B4BDB882ACBED2100E55D78 /* SettingsAccess */,
);
productName = TypeaheadAI;
@@ -448,7 +448,7 @@
2BA3C2332AADAC5700537F95 /* XCRemoteSwiftPackageReference "llama" */,
2BF9297A2AB13EEA00FC105B /* XCRemoteSwiftPackageReference "swift-markdown" */,
2BF929832AB13FEC00FC105B /* XCRemoteSwiftPackageReference "HighlighterSwift" */,
- 2BF558BC2AB8353B002F2008 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
+ 2B285D832ACA22FB000C5BDE /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */,
2B4BDB872ACBED2100E55D78 /* XCRemoteSwiftPackageReference "SettingsAccess" */,
);
productRefGroup = 2BA7F0762A9ABBA8003D38BA /* Products */;
@@ -888,6 +888,14 @@
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
+ 2B285D832ACA22FB000C5BDE /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
2B473E8A2AA860380042913D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/orchetect/MenuBarExtraAccess";
@@ -920,14 +928,6 @@
minimumVersion = 1.0.0;
};
};
- 2BF558BC2AB8353B002F2008 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = {
- isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern";
- requirement = {
- branch = main;
- kind = branch;
- };
- };
2BF9297A2AB13EEA00FC105B /* XCRemoteSwiftPackageReference "swift-markdown" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-markdown";
@@ -947,6 +947,11 @@
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
+ 2B285D842ACA22FB000C5BDE /* LaunchAtLogin */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 2B285D832ACA22FB000C5BDE /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */;
+ productName = LaunchAtLogin;
+ };
2B473E8B2AA860380042913D /* MenuBarExtraAccess */ = {
isa = XCSwiftPackageProductDependency;
package = 2B473E8A2AA860380042913D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */;
@@ -967,11 +972,6 @@
package = 2BA7F0AC2A9ABC47003D38BA /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */;
productName = KeyboardShortcuts;
};
- 2BF558BD2AB8353B002F2008 /* LaunchAtLogin */ = {
- isa = XCSwiftPackageProductDependency;
- package = 2BF558BC2AB8353B002F2008 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */;
- productName = LaunchAtLogin;
- };
2BF9297B2AB13EEA00FC105B /* Markdown */ = {
isa = XCSwiftPackageProductDependency;
package = 2BF9297A2AB13EEA00FC105B /* XCRemoteSwiftPackageReference "swift-markdown" */;
diff --git a/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist b/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist
index 009bde8..d5ea02b 100644
--- a/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/TypeaheadAI.xcodeproj/xcuserdata/jeffhara.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,7 +7,7 @@
TypeaheadAI.xcscheme_^#shared#^_
orderHint
- 1
+ 0
diff --git a/TypeaheadAI/ClientManager.swift b/TypeaheadAI/ClientManager.swift
index b37afd3..ec64113 100644
--- a/TypeaheadAI/ClientManager.swift
+++ b/TypeaheadAI/ClientManager.swift
@@ -405,10 +405,25 @@ class ClientManager {
version: "onboarding_v3"
)
- if let result: Result = await self?.performOnboardingTask(payload: payload, timeout: timeout, streamHandler: streamHandler) {
- completion(result)
- } else {
- completion(.failure(ClientManagerError.appError("Something went wrong...")))
+ do {
+ let stream = try self?.performOnboardingTask(
+ payload: payload,
+ timeout: timeout,
+ completion: completion
+ )
+
+ guard let stream = stream else {
+ self?.logger.debug("Failed to get stream")
+ streamHandler(.failure(ClientManagerError.networkError("Failed to connect")))
+ return
+ }
+
+ for try await text in stream {
+ self?.logger.debug("stream: \(text)")
+ streamHandler(.success(text))
+ }
+ } catch {
+ streamHandler(.failure(error))
}
}
}
@@ -592,37 +607,34 @@ class ClientManager {
private func performOnboardingTask(
payload: OnboardingRequestPayload,
timeout: TimeInterval,
- streamHandler: @escaping (Result) -> Void
- ) async -> Result {
+ completion: @escaping (Result) -> Void
+ ) throws -> AsyncThrowingStream {
guard let httpBody = try? JSONEncoder().encode(payload) else {
- let error: Result = .failure(ClientManagerError.badRequest("Encoding error"))
- streamHandler(error)
- return error
+ throw ClientManagerError.badRequest("Encoding error")
}
- var request = URLRequest(url: self.apiOnboarding, timeoutInterval: timeout)
- request.httpMethod = "POST"
- request.httpBody = httpBody
- request.addValue("application/json", forHTTPHeaderField: "Content-Type")
-
- var output = ""
- do {
- let (stream, _) = try await URLSession.shared.bytes(for: request)
+ return AsyncThrowingStream { continuation in
+ Task {
+ var urlRequest = URLRequest(url: self.apiOnboarding, timeoutInterval: timeout)
+ urlRequest.httpMethod = "POST"
+ urlRequest.httpBody = httpBody
+ urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
- for try await line in stream.lines {
- let decodedResponse = try JSONDecoder().decode(ChunkPayload.self, from: line.data(using: .utf8)!)
- if let text = decodedResponse.text {
- output += text
- streamHandler(.success(text))
+ let (result, _) = try await URLSession.shared.bytes(for: urlRequest)
+ var output = ""
+ for try await line in result.lines {
+ if let data = line.data(using: .utf8),
+ let response = try? JSONDecoder().decode(ChunkPayload.self, from: data),
+ let text = response.text {
+ output += text
+ continuation.yield(text)
+ }
}
+
+ completion(.success(output))
+ continuation.finish()
}
- } catch {
- let err: Result = .failure(error)
- streamHandler(err)
- return err
}
-
- return .success(output)
}
private func performStreamOfflineTask(
diff --git a/TypeaheadAI/Llama/LlamaWrapper.swift b/TypeaheadAI/Llama/LlamaWrapper.swift
index 82fd4e0..c1d589c 100644
--- a/TypeaheadAI/Llama/LlamaWrapper.swift
+++ b/TypeaheadAI/Llama/LlamaWrapper.swift
@@ -50,6 +50,28 @@ class LlamaWrapper {
return model != nil
}
+ func stream(
+ _ prompt: String
+ ) -> AsyncThrowingStream {
+ ctx = llama_new_context_with_model(model, params)
+ return AsyncThrowingStream { continuation in
+ do {
+ try simple_predict(ctx, prompt, 1) { string in
+ continuation.yield(string)
+ }
+ continuation.finish()
+ } catch {
+ continuation.finish(throwing: error)
+ }
+ }
+
+ guard let cstr = simple_predict(ctx, prompt, 1, globalHandler) else {
+ throw LlamaWrapperError.serverError("Failed to run simple_predict")
+ }
+
+
+ }
+
func predict(
_ prompt: String,
handler: @escaping (Result) -> Void
diff --git a/TypeaheadAI/ModalManager.swift b/TypeaheadAI/ModalManager.swift
index 79d7eb3..da8794e 100644
--- a/TypeaheadAI/ModalManager.swift
+++ b/TypeaheadAI/ModalManager.swift
@@ -134,7 +134,7 @@ class ModalManager: ObservableObject {
/// Set an error message.
@MainActor
- func setError(_ responseError: String) {
+ func setError(_ responseError: String) async {
isPending = false
if let idx = messages.indices.last, !messages[idx].isCurrentUser {
@@ -277,15 +277,11 @@ class ModalManager: ObservableObject {
switch result {
case .success(let chunk):
- Task {
- await self.appendText(chunk)
- }
self.logger.info("Received chunk: \(chunk)")
+ await self.appendText(chunk)
case .failure(let error):
- Task {
- self.setError(error.localizedDescription)
- }
self.logger.error("An error occurred: \(error)")
+ await self.setError(error.localizedDescription)
}
}, completion: defaultCompletionHandler)
}
@@ -482,28 +478,18 @@ class ModalManager: ObservableObject {
func defaultHandler(result: Result) {
switch result {
case .success(let chunk):
- Task {
- await self.appendText(chunk)
- }
self.logger.info("Received chunk: \(chunk)")
+ await self.appendText(chunk)
case .failure(let error as ClientManagerError):
self.logger.error("Error: \(error.localizedDescription)")
switch error {
case .badRequest(let message):
- DispatchQueue.main.async {
- self.setError(message)
- }
+ await self.setError(message)
default:
- DispatchQueue.main.async {
- self.setError("Something went wrong. Please try again.")
- }
- self.logger.error("Something went wrong.")
+ await self.setError("Something went wrong. Please try again.")
}
case .failure(let error):
- self.logger.error("Error: \(error.localizedDescription)")
- DispatchQueue.main.async {
- self.setError(error.localizedDescription)
- }
+ await self.setError(error.localizedDescription)
}
}
}
diff --git a/TypeaheadAI/TypeaheadAIApp.swift b/TypeaheadAI/TypeaheadAIApp.swift
index 8975d05..8df06ab 100644
--- a/TypeaheadAI/TypeaheadAIApp.swift
+++ b/TypeaheadAI/TypeaheadAIApp.swift
@@ -16,10 +16,8 @@ import MenuBarExtraAccess
@MainActor
final class AppState: ObservableObject {
@Published var isLoading: Bool = false
- @Published var isBlinking: Bool = false
@Published var isMenuVisible: Bool = false
- private var blinkTimer: Timer?
let logger = Logger(
subsystem: "ai.typeahead.TypeaheadAI",
category: "AppState"
@@ -167,22 +165,6 @@ final class AppState: ObservableObject {
mouseEventMonitor.stopMonitoring()
}
- private func startBlinking() {
- // Invalidate the previous timer if it exists
- blinkTimer?.invalidate()
-
- // Create and schedule a new timer
- blinkTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
- DispatchQueue.main.async { self.isBlinking.toggle() }
- }
- }
-
- private func stopBlinking() {
- blinkTimer?.invalidate()
- blinkTimer = nil
- DispatchQueue.main.async { self.isBlinking = false }
- }
-
private func checkAndRequestNotificationPermissions() -> Void {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if granted {
@@ -197,15 +179,11 @@ final class AppState: ObservableObject {
@main
struct TypeaheadAIApp {
static func main() {
- if #available(macOS 13.0, *) {
- if UserDefaults.standard.bool(forKey: "hasOnboardedV3") {
- MacOS13AndLaterApp.main()
- } else {
- UserDefaults.standard.setValue(true, forKey: "hasOnboardedV3")
- MacOS13AndLaterAppWithOnboardingV2.main()
- }
+ if false {//if UserDefaults.standard.bool(forKey: "hasOnboardedV3") {
+ MacOS13AndLaterApp.main()
} else {
- MacOS12AndEarlierApp.main()
+ UserDefaults.standard.setValue(true, forKey: "hasOnboardedV3")
+ MacOS13AndLaterAppWithOnboardingV2.main()
}
}
}
@@ -222,58 +200,6 @@ struct SettingsScene: Scene {
}
}
-struct MacOS12AndEarlierApp: App {
- @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
-
- var body: some Scene {
- SettingsScene(appState: appDelegate.appState)
- }
-}
-
-class AppDelegate: NSObject, NSApplicationDelegate {
- var statusBarItem: NSStatusItem?
- var application: NSApplication = NSApplication.shared
-
- let persistenceController = PersistenceController.shared
- @ObservedObject var appState: AppState = {
- let context = PersistenceController.shared.container.viewContext
- return AppState(context: context)
- }()
-
- override init() {
- // Further customization if needed.
- super.init()
- }
-
- func applicationDidFinishLaunching(_ notification: Notification) {
- let menu = NSMenu()
- let menuItem = NSMenuItem()
-
- // SwiftUI View
- let subview = CommonMenuView(
- promptManager: appState.promptManager,
- modalManager: appState.modalManager,
- isMenuVisible: $appState.isMenuVisible
- )
-
- let view = NSHostingView(rootView: subview)
- view.becomeFirstResponder()
-
- // Very important! If you don't set the frame the menu won't appear to open.
- view.frame = NSRect(x: 0, y: 0, width: 300, height: 400)
- menuItem.view = view
-
- menu.addItem(menuItem)
-
- NSApp.activate(ignoringOtherApps: true)
-
- statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
- statusBarItem?.button?.image = NSImage(systemSymbolName: appState.isBlinking ? "list.clipboard.fill" : "list.clipboard", accessibilityDescription: nil)
- statusBarItem?.menu = menu
- }
-}
-
-@available(macOS 13.0, *)
struct MacOS13AndLaterApp: App {
let persistenceController = PersistenceController.shared
@StateObject var appState: AppState
@@ -294,7 +220,7 @@ struct MacOS13AndLaterApp: App {
isMenuVisible: $appState.isMenuVisible
)
} label: {
- Image(systemName: appState.isBlinking ? "list.clipboard.fill" : "list.clipboard")
+ Image(systemName: "list.clipboard")
// TODO: Add symbolEffect when available
}
.menuBarExtraAccess(isPresented: $appState.isMenuVisible)
@@ -302,7 +228,6 @@ struct MacOS13AndLaterApp: App {
}
}
-@available(macOS 13.0, *)
struct MacOS13AndLaterAppWithOnboardingV2: App {
let persistenceController = PersistenceController.shared
@StateObject var appState: AppState
@@ -317,7 +242,11 @@ struct MacOS13AndLaterAppWithOnboardingV2: App {
OnboardingView(
modalManager: appState.modalManager
)
+ .onAppear {
+ NSApp.activate(ignoringOtherApps: true)
+ }
}
+ .windowStyle(.hiddenTitleBar)
SettingsScene(appState: appState)
@@ -328,7 +257,7 @@ struct MacOS13AndLaterAppWithOnboardingV2: App {
isMenuVisible: $appState.isMenuVisible
)
} label: {
- Image(systemName: appState.isBlinking ? "list.clipboard.fill" : "list.clipboard")
+ Image(systemName: "list.clipboard")
// TODO: Add symbolEffect when available
}
.menuBarExtraAccess(isPresented: $appState.isMenuVisible)
diff --git a/TypeaheadAI/Views/Onboarding/OnboardingView.swift b/TypeaheadAI/Views/Onboarding/OnboardingView.swift
index 0ab7267..da7fea7 100644
--- a/TypeaheadAI/Views/Onboarding/OnboardingView.swift
+++ b/TypeaheadAI/Views/Onboarding/OnboardingView.swift
@@ -8,6 +8,11 @@
import SwiftUI
import AuthenticationServices
+struct VisualEffect: NSViewRepresentable {
+ func makeNSView(context: Self.Context) -> NSView { return NSVisualEffectView() }
+ func updateNSView(_ nsView: NSView, context: Context) { }
+}
+
struct OnboardingView: View {
@ObservedObject var modalManager: ModalManager
@State private var messages: [Message] = []
@@ -155,6 +160,7 @@ struct OnboardingView: View {
}
.padding()
}
+ .background(VisualEffect().ignoresSafeArea())
}
/// Append text to the onboarding messages. Creates a new message if there is nothing to append to.
diff --git a/TypeaheadAI/Views/Settings/AccountView.swift b/TypeaheadAI/Views/Settings/AccountView.swift
index f5d97a9..cea7cf5 100644
--- a/TypeaheadAI/Views/Settings/AccountView.swift
+++ b/TypeaheadAI/Views/Settings/AccountView.swift
@@ -7,6 +7,7 @@
import SwiftUI
import AuthenticationServices
+import LaunchAtLogin
struct AccountView: View {
@AppStorage("token") var token: String?
@@ -20,7 +21,7 @@ struct AccountView: View {
Divider()
- Text(token == nil ? "You are not signed in." : "You're signed in through Apple iCloud!")
+ Text(token == nil ? "You are not signed in." : "You're signed in with Apple iCloud.")
if token == nil {
SignInWithAppleButton(.signIn) { request in
@@ -45,6 +46,10 @@ struct AccountView: View {
token = nil
}
}
+
+ Spacer()
+
+ LaunchAtLogin.Toggle()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding(10)