Skip to content
Open
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
27 changes: 25 additions & 2 deletions Dayflow/Dayflow.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
C4C1D29D2DB56805007D5A55 /* Sources */,
C4C1D29E2DB56805007D5A55 /* Frameworks */,
C4C1D29F2DB56805007D5A55 /* Resources */,
0AEE4CB1C67C42909CFA55F6 /* Re-sign Sparkle Installer.xpc for Debug */,
);
buildRules = (
);
Expand Down Expand Up @@ -243,6 +244,27 @@
};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
0AEE4CB1C67C42909CFA55F6 /* Re-sign Sparkle Installer.xpc for Debug */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Re-sign Sparkle Installer.xpc for Debug";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if [ \"$CONFIGURATION\" != \"Debug\" ]; then\n exit 0\nfi\n\nENTITLEMENTS=\"${SRCROOT}/Dayflow/Debug.entitlements\"\nXPC_BUNDLE=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework/Versions/B/XPCServices/Installer.xpc\"\n\nif [ ! -f \"$ENTITLEMENTS\" ]; then\n echo \"warning: Debug.entitlements not found at $ENTITLEMENTS\"\n exit 0\nfi\n\nif [ -d \"$XPC_BUNDLE\" ]; then\n codesign --force --sign - --entitlements \"$ENTITLEMENTS\" \"$XPC_BUNDLE\"\nelse\n echo \"warning: Installer.xpc not found at $XPC_BUNDLE\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
C4C1D29D2DB56805007D5A55 /* Sources */ = {
isa = PBXSourcesBuildPhase;
Expand Down Expand Up @@ -412,9 +434,10 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_ASSET_PATHS = "\"Dayflow/Preview Content\"";
DEVELOPMENT_TEAM = L75WYD8X4Y;
DEVELOPMENT_TEAM = VS3FLTY94C;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Dayflow/Info.plist;
INFOPLIST_KEY_LSUIElement = NO;
Expand Down Expand Up @@ -446,7 +469,7 @@
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 107;
DEVELOPMENT_ASSET_PATHS = "\"Dayflow/Preview Content\"";
DEVELOPMENT_TEAM = L75WYD8X4Y;
DEVELOPMENT_TEAM = VS3FLTY94C;
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down
2 changes: 2 additions & 0 deletions Dayflow/Dayflow/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

// Start daily recap generation scheduler (checks every 5 minutes)
DailyRecapScheduler.shared.start()
PersonalPlugins.start()

// Observe recording state
analyticsSub = AppState.shared.$isRecording
Expand Down Expand Up @@ -260,6 +261,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
case .dayflowBackend: return "dayflow"
case .ollamaLocal: return "ollama"
case .chatGPTClaude: return "chat_cli"
case .apfel: return "apfel"
}
}()
])
Expand Down
16 changes: 16 additions & 0 deletions Dayflow/Dayflow/App/PersonalPlugins.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// PersonalPlugins.swift
// Dayflow
//
// Single entry point for all personal extensions.
// AppDelegate calls PersonalPlugins.start() once — all future plugins
// register here without any additional upstream file changes.
//

import Foundation

enum PersonalPlugins {
static func start() {
// Register plugins below — one line per feature, all in this file.
}
}
106 changes: 106 additions & 0 deletions Dayflow/Dayflow/Core/AI/Apfel/ApfelLanguageModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// ApfelLanguageModel.swift
// Dayflow
//
// Wraps Apple's on-device language model (FoundationModels, macOS 26+).
// Availability-gated so the binary still builds and runs on older macOS;
// `isAvailable` is the single runtime check callers use before constructing
// anything that actually talks to the model.
//

import Foundation

#if canImport(FoundationModels)
import FoundationModels
#endif

enum ApfelAvailability: Equatable {
case available
case unsupportedOS
case modelNotReady
case appleIntelligenceDisabled
case unknown(String)

var isAvailable: Bool {
if case .available = self { return true }
return false
}

var userFacingReason: String {
switch self {
case .available:
return "Available"
case .unsupportedOS:
return "Requires macOS 26 or later."
case .modelNotReady:
return "Apple Intelligence is still downloading or preparing."
case .appleIntelligenceDisabled:
return "Enable Apple Intelligence in System Settings to use this provider."
case .unknown(let detail):
return detail
}
}
}

struct ApfelLanguageModel {
static var availability: ApfelAvailability {
if #available(macOS 26.0, *) {
#if canImport(FoundationModels)
return SystemLanguageModel.default.availability == .available
? .available
: .modelNotReady
#else
return .unsupportedOS
#endif
}
return .unsupportedOS
}

/// Generate a single text completion for the given prompt.
/// Throws if Apple Intelligence is unavailable on this device/OS.
func generate(prompt: String, systemInstructions: String? = nil) async throws -> String {
guard #available(macOS 26.0, *) else {
throw ApfelRuntimeError.unsupportedOS
}
#if canImport(FoundationModels)
return try await Self.generateOnSupportedOS(
prompt: prompt, systemInstructions: systemInstructions)
#else
throw ApfelRuntimeError.frameworkMissing
#endif
}

#if canImport(FoundationModels)
@available(macOS 26.0, *)
private static func generateOnSupportedOS(
prompt: String, systemInstructions: String?
) async throws -> String {
let session: LanguageModelSession
if let systemInstructions {
session = LanguageModelSession(instructions: systemInstructions)
} else {
session = LanguageModelSession()
}
let response = try await session.respond(to: prompt)
return response.content
}
#endif

}

enum ApfelRuntimeError: Error, LocalizedError {
case unsupportedOS
case frameworkMissing
case generationFailed(String)

var errorDescription: String? {
switch self {
case .unsupportedOS:
return "Apple Intelligence requires macOS 26 or later."
case .frameworkMissing:
return "FoundationModels framework is not available in this build."
case .generationFailed(let detail):
return "Apple Intelligence failed to generate a response: \(detail)"
}
}
}
Loading