Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af644456e |
-29
@@ -1,29 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Only the latest publicly released version of oAI is supported with security fixes. Please update to the latest version before reporting an issue, and confirm it still reproduces there.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in oAI, please report it privately rather than opening a public GitHub issue.
|
||||
|
||||
To report a security concern, use the contact form at **[https://oai.pm/#contact](https://oai.pm/#contact)**.
|
||||
|
||||
Please include as much detail as possible:
|
||||
- A description of the vulnerability and its potential impact
|
||||
- Steps to reproduce the issue
|
||||
- The oAI version and macOS version you're using
|
||||
- Any relevant logs (`~/Library/Logs/oAI.log`), with sensitive data redacted
|
||||
|
||||
## Scope
|
||||
|
||||
oAI is a native macOS app that stores conversations, settings, and API keys locally (SQLite database and Keychain). Areas of particular interest for security reports include:
|
||||
- API key handling and Keychain storage
|
||||
- MCP file access permission checks
|
||||
- Bash execution approval flow
|
||||
- Any path that could lead to data exfiltration or unauthorized local file/system access
|
||||
|
||||
## Response
|
||||
|
||||
Reports submitted through the contact form will be reviewed and acknowledged as soon as possible. Please allow time for a fix to be developed and released before any public disclosure.
|
||||
@@ -15,22 +15,9 @@
|
||||
A550A6622F3B72EA00136F2B /* oAI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = oAI.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
911C4D0E69E11B84C61453DC /* Exceptions for "oAI" folder in "oAI" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = A550A6612F3B72EA00136F2B /* oAI */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
A550A6642F3B72EA00136F2B /* oAI */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
911C4D0E69E11B84C61453DC /* Exceptions for "oAI" folder in "oAI" target */,
|
||||
);
|
||||
path = oAI;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -283,7 +270,6 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "oAI/Info.plist";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
||||
@@ -303,7 +289,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -333,7 +319,6 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readonly;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "oAI/Info.plist";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = "oAI can read and create calendar events when you ask it to, if you enable Calendar access in Settings.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "oAI can search your contacts when you ask it to, if you enable Contacts access in Settings.";
|
||||
@@ -353,7 +338,7 @@
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 2.4.2;
|
||||
MARKETING_VERSION = 2.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
|
||||
@@ -20,11 +20,7 @@
|
||||
|
||||
<nav class="toc">
|
||||
<h2>Contents</h2>
|
||||
<div class="toc-search">
|
||||
<input type="search" id="tocSearch" placeholder="Search help topics…" aria-label="Search help topics">
|
||||
</div>
|
||||
<p id="tocNoResults" class="toc-no-results" hidden>No topics match your search.</p>
|
||||
<ul id="tocList">
|
||||
<ul>
|
||||
<li><a href="#getting-started">Getting Started</a></li>
|
||||
<li><a href="#providers">AI Providers & API Keys</a></li>
|
||||
<li><a href="#models">Selecting Models</a></li>
|
||||
@@ -1530,6 +1526,9 @@ Whenever the user asks you to translate something, translate it to Norwegian Bok
|
||||
<dt><kbd>⌘,</kbd></dt>
|
||||
<dd>Open Settings</dd>
|
||||
|
||||
<dt><kbd>⌘/</kbd></dt>
|
||||
<dd>Show in-app Help</dd>
|
||||
|
||||
<dt><kbd>⌘?</kbd></dt>
|
||||
<dd>Open this Help (macOS Help)</dd>
|
||||
|
||||
@@ -1816,37 +1815,5 @@ Whenever the user asks you to translate something, translate it to Norwegian Bok
|
||||
<p>© 2026 oAI - Rune Olsen. For support or feedback, visit <a href="https://gitlab.pm/rune/oai-swift">gitlab.pm</a> or <a href="mailto:support@fubar.pm?subject=oAI Support&body=What can I help you with?">Contact Us</a>.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var searchInput = document.getElementById('tocSearch');
|
||||
var tocList = document.getElementById('tocList');
|
||||
var noResults = document.getElementById('tocNoResults');
|
||||
if (!searchInput || !tocList) return;
|
||||
|
||||
var entries = Array.prototype.map.call(tocList.querySelectorAll('li'), function (li) {
|
||||
var link = li.querySelector('a');
|
||||
var id = link ? link.getAttribute('href').slice(1) : null;
|
||||
var section = id ? document.getElementById(id) : null;
|
||||
return {
|
||||
li: li,
|
||||
text: (section ? section.textContent : li.textContent).toLowerCase()
|
||||
};
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function () {
|
||||
var query = searchInput.value.trim().toLowerCase();
|
||||
var visibleCount = 0;
|
||||
|
||||
entries.forEach(function (entry) {
|
||||
var matches = query === '' || entry.text.indexOf(query) !== -1;
|
||||
entry.li.hidden = !matches;
|
||||
if (matches) visibleCount++;
|
||||
});
|
||||
|
||||
noResults.hidden = visibleCount > 0;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -101,32 +101,6 @@ nav.toc h2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toc-search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.toc-search input[type="search"] {
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
padding: 10px 14px;
|
||||
color: var(--text-primary);
|
||||
background: var(--background);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toc-search input[type="search"]:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.toc-no-results {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
nav.toc ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -34,15 +34,6 @@ struct BackupManifest: Codable {
|
||||
let credentials: [String: String]?
|
||||
}
|
||||
|
||||
// MARK: - FavoritesPayload
|
||||
|
||||
/// Small standalone file (separate from the full settings backup) so starring a model
|
||||
/// syncs near-instantly across machines instead of waiting for the next full backup.
|
||||
struct FavoritesPayload: Codable {
|
||||
let updatedAt: String
|
||||
let ids: [String]
|
||||
}
|
||||
|
||||
// MARK: - BackupService
|
||||
|
||||
@Observable
|
||||
@@ -60,8 +51,6 @@ final class BackupService {
|
||||
/// URL of the last backup file
|
||||
var lastBackupURL: URL?
|
||||
|
||||
private var autoBackupTimer: Timer?
|
||||
|
||||
// Keys excluded from backup — encrypted_ prefix + internal migration flags
|
||||
private static let excludedKeys: Set<String> = [
|
||||
"encrypted_openrouterAPIKey",
|
||||
@@ -82,7 +71,6 @@ final class BackupService {
|
||||
|
||||
private init() {
|
||||
checkForExistingBackup()
|
||||
startAutoBackupTimer()
|
||||
}
|
||||
|
||||
// MARK: - iCloud Path Resolution
|
||||
@@ -188,88 +176,6 @@ final class BackupService {
|
||||
log.info("Restored \(manifest.settings.count) settings from backup (v\(manifest.version))")
|
||||
}
|
||||
|
||||
// MARK: - Favorite Models Sync
|
||||
|
||||
private func favoritesFileURL() -> URL {
|
||||
resolveBackupDirectory().appendingPathComponent("oai_favorites.json")
|
||||
}
|
||||
|
||||
/// Write the current local favorites to iCloud Drive. Called whenever a favorite is toggled.
|
||||
func pushFavorites() async {
|
||||
let settings = SettingsService.shared
|
||||
let payload = FavoritesPayload(
|
||||
updatedAt: settings.favoriteModelsUpdatedAt,
|
||||
ids: settings.favoriteModelIds.sorted()
|
||||
)
|
||||
guard let data = try? JSONEncoder().encode(payload) else { return }
|
||||
try? data.write(to: favoritesFileURL(), options: .atomic)
|
||||
log.debug("Pushed \(payload.ids.count) favorite model(s) to iCloud")
|
||||
}
|
||||
|
||||
/// Reconcile local favorites with the iCloud copy using last-write-wins (by timestamp).
|
||||
/// Call on launch (and optionally on app-become-active) to pick up changes from other machines.
|
||||
func syncFavoritesOnLaunch() async {
|
||||
let settings = SettingsService.shared
|
||||
let fileURL = favoritesFileURL()
|
||||
|
||||
guard let data = try? Data(contentsOf: fileURL),
|
||||
let remote = try? JSONDecoder().decode(FavoritesPayload.self, from: data) else {
|
||||
// No remote copy yet — push local state (may be empty, that's fine).
|
||||
await pushFavorites()
|
||||
return
|
||||
}
|
||||
|
||||
let localUpdatedAt = settings.favoriteModelsUpdatedAt
|
||||
let localIds = settings.favoriteModelIds
|
||||
if remote.updatedAt > localUpdatedAt {
|
||||
settings.favoriteModelIds = Set(remote.ids)
|
||||
settings.favoriteModelsUpdatedAt = remote.updatedAt
|
||||
log.info("Applied \(remote.ids.count) favorite model(s) from iCloud (remote was newer)")
|
||||
} else if localUpdatedAt > remote.updatedAt {
|
||||
await pushFavorites()
|
||||
} else if Set(remote.ids) != localIds {
|
||||
// Tied timestamps (both empty is the common case: two machines that already had
|
||||
// favorites before this sync feature existed, neither ever bumped the timestamp).
|
||||
// Union rather than silently dropping one side's favorites.
|
||||
settings.favoriteModelIds = localIds.union(remote.ids)
|
||||
settings.favoriteModelsUpdatedAt = ISO8601DateFormatter().string(from: Date())
|
||||
await pushFavorites()
|
||||
log.info("Merged favorites from iCloud (tied timestamps) — union of \(localIds.count) local + \(remote.ids.count) remote")
|
||||
}
|
||||
// Equal timestamps and identical sets: already in sync, nothing to do.
|
||||
}
|
||||
|
||||
// MARK: - Automatic Backup
|
||||
|
||||
private static let dailyInterval: TimeInterval = 24 * 3600
|
||||
private static let weeklyInterval: TimeInterval = 7 * 24 * 3600
|
||||
|
||||
/// Checked once at launch and hourly thereafter while the app is running.
|
||||
private func startAutoBackupTimer() {
|
||||
Task { await checkAndPerformAutoBackupIfDue() }
|
||||
autoBackupTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true) { [weak self] _ in
|
||||
Task { await self?.checkAndPerformAutoBackupIfDue() }
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndPerformAutoBackupIfDue() async {
|
||||
let frequency = SettingsService.shared.autoBackupFrequency
|
||||
let interval: TimeInterval
|
||||
switch frequency {
|
||||
case "daily": interval = Self.dailyInterval
|
||||
case "weekly": interval = Self.weeklyInterval
|
||||
default: return // "manual" — user triggers backups by hand
|
||||
}
|
||||
|
||||
checkForExistingBackup()
|
||||
if let last = lastBackupDate, Date().timeIntervalSince(last) < interval {
|
||||
return // not due yet
|
||||
}
|
||||
|
||||
log.info("Automatic backup (\(frequency, privacy: .public)) is due — backing up now")
|
||||
_ = try? await exportSettings()
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func appVersion() -> String {
|
||||
|
||||
@@ -63,8 +63,7 @@ class ContactsService {
|
||||
store.requestAccess(for: .contacts) { granted, error in
|
||||
let after = CNContactStore.authorizationStatus(for: .contacts)
|
||||
if let error {
|
||||
let nsError = error as NSError
|
||||
Log.mcp.error("ContactsService.requestAccess: error=\(error.localizedDescription) domain=\(nsError.domain) code=\(nsError.code) userInfo=\(nsError.userInfo); granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
|
||||
Log.mcp.error("ContactsService.requestAccess: error=\(error.localizedDescription); granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
|
||||
} else {
|
||||
Log.mcp.info("ContactsService.requestAccess: granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
|
||||
}
|
||||
|
||||
@@ -25,11 +25,16 @@ import Foundation
|
||||
import os
|
||||
import Security
|
||||
|
||||
/// Kill switch for the entire Personal Data tools section (Calendar/Reminders/Contacts/
|
||||
/// Location & Maps). Hides the UI and forces every `*Enabled` getter to return `false`
|
||||
/// regardless of the persisted DB value — no code deleted, just inert.
|
||||
/// Kill switch for the Personal Data tools (Calendar/Reminders/Contacts/Location & Maps).
|
||||
/// Flip `isHiddenPendingAppleFix` back to `false` once Apple fixes the macOS 27 beta TCC bug
|
||||
/// (filed with Apple). Each flag hides the relevant UI and forces the `*Enabled` getter to
|
||||
/// return `false` regardless of the persisted DB value — no code deleted, just inert.
|
||||
enum PersonalDataTools {
|
||||
/// Hides the entire Personal Data section. Flip to `false` once all four services work.
|
||||
static let isHiddenPendingAppleFix = false
|
||||
/// Hides only the Contacts row. Contacts TCC still broken under hardened runtime on
|
||||
/// macOS 27 beta 2 while Calendar/Reminders/Location are fixed. Flip to `false` once fixed.
|
||||
static let isContactsHiddenPendingAppleFix = true
|
||||
}
|
||||
|
||||
@Observable
|
||||
@@ -515,33 +520,10 @@ class SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
/// ISO8601 timestamp of the last local change to favoriteModelIds — used to
|
||||
/// resolve last-write-wins conflicts when syncing favorites across machines.
|
||||
var favoriteModelsUpdatedAt: String {
|
||||
get { cache["favoriteModelsUpdatedAt"] ?? "" }
|
||||
set {
|
||||
cache["favoriteModelsUpdatedAt"] = newValue
|
||||
DatabaseService.shared.setSetting(key: "favoriteModelsUpdatedAt", value: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func toggleFavoriteModel(_ id: String) {
|
||||
var favs = favoriteModelIds
|
||||
if favs.contains(id) { favs.remove(id) } else { favs.insert(id) }
|
||||
favoriteModelIds = favs
|
||||
favoriteModelsUpdatedAt = ISO8601DateFormatter().string(from: Date())
|
||||
Task { await BackupService.shared.pushFavorites() }
|
||||
}
|
||||
|
||||
// MARK: - Automatic Backup
|
||||
|
||||
/// "manual" (default), "daily", or "weekly"
|
||||
var autoBackupFrequency: String {
|
||||
get { cache["autoBackupFrequency"] ?? "manual" }
|
||||
set {
|
||||
cache["autoBackupFrequency"] = newValue
|
||||
DatabaseService.shared.setSetting(key: "autoBackupFrequency", value: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Anytype MCP Settings
|
||||
@@ -713,7 +695,7 @@ class SettingsService {
|
||||
}
|
||||
|
||||
var contactsEnabled: Bool {
|
||||
get { !PersonalDataTools.isHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
|
||||
get { !PersonalDataTools.isHiddenPendingAppleFix && !PersonalDataTools.isContactsHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
|
||||
set {
|
||||
cache["contactsEnabled"] = String(newValue)
|
||||
DatabaseService.shared.setSetting(key: "contactsEnabled", value: String(newValue))
|
||||
|
||||
@@ -90,7 +90,6 @@ You are a helpful AI assistant. Follow these core principles:
|
||||
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
|
||||
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did.
|
||||
- **Complete Tasks Properly**: If you start something, finish it correctly.
|
||||
- **Match the User's Language**: Always reply in the same language the user is writing in, even when the request itself involves another language (e.g. a translation or spelling request) — the target language applies to the content you produce, not to your own reply.
|
||||
|
||||
## FORMATTING
|
||||
|
||||
|
||||
@@ -29,8 +29,7 @@ import Darwin // uname, sysctlbyname
|
||||
struct ContentView: View {
|
||||
@Environment(ChatViewModel.self) var chatViewModel
|
||||
private var updateService = UpdateCheckService.shared
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility =
|
||||
SettingsService.shared.sidebarVisible ? .all : .detailOnly
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
||||
@State private var showIntelWarning = false
|
||||
|
||||
var body: some View {
|
||||
@@ -47,9 +46,6 @@ struct ContentView: View {
|
||||
)
|
||||
}
|
||||
.frame(minWidth: 860, minHeight: 560)
|
||||
.onChange(of: columnVisibility) { _, newValue in
|
||||
SettingsService.shared.sidebarVisible = newValue != .detailOnly
|
||||
}
|
||||
#if os(macOS)
|
||||
.onAppear {
|
||||
NSApplication.shared.windows.forEach { $0.tabbingMode = .disallowed }
|
||||
|
||||
@@ -213,6 +213,7 @@ private let keyboardShortcuts: [(key: String, description: String)] = [
|
||||
("\u{2318}L", "Conversations"),
|
||||
("\u{21E7}\u{2318}S", "Statistics"),
|
||||
("\u{2318},", "Settings"),
|
||||
("\u{2318}/", "Help"),
|
||||
]
|
||||
|
||||
// MARK: - HelpView
|
||||
|
||||
@@ -125,7 +125,6 @@ You are a helpful AI assistant. Follow these core principles:
|
||||
- **Be Direct**: Provide concise, relevant answers. No unnecessary preambles.
|
||||
- **Show Your Work**: If you use capabilities (tools, web search, etc.), demonstrate what you did.
|
||||
- **Complete Tasks Properly**: If you start something, finish it correctly.
|
||||
- **Match the User's Language**: Always reply in the same language the user is writing in, even when the request itself involves another language (e.g. a translation or spelling request) — the target language applies to the content you produce, not to your own reply.
|
||||
|
||||
## FORMATTING
|
||||
|
||||
@@ -829,6 +828,10 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
externalMCPSection
|
||||
|
||||
// MARK: Personal Data
|
||||
// isHiddenPendingAppleFix hides the entire section (macOS 27 beta TCC bug).
|
||||
// isContactsHiddenPendingAppleFix hides just the Contacts row (still broken in beta 2
|
||||
// under hardened runtime while Calendar/Reminders/Location are fixed). Flip each flag
|
||||
// to false once Apple ships a fix.
|
||||
if !PersonalDataTools.isHiddenPendingAppleFix {
|
||||
Divider()
|
||||
|
||||
@@ -839,8 +842,18 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
.foregroundStyle(.teal)
|
||||
Text("Personal Data")
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
if PersonalDataTools.isContactsHiddenPendingAppleFix {
|
||||
Text("β")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(.orange)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.orange.opacity(0.15))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.help("Beta Feature. May change.")
|
||||
}
|
||||
Text("Let the AI access your Calendar, Reminders, Contacts, and Location & Maps to answer questions about your schedule and surroundings. Each service is opt-in and uses standard macOS permission prompts. This functionality is in beta and may change.")
|
||||
}
|
||||
Text("Let the AI access your Calendar, Reminders, and Location & Maps to answer questions about your schedule and surroundings. Each service is opt-in and uses standard macOS permission prompts. This functionality is in beta and may change.")
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -875,6 +888,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
requestAccess: { remindersAccessState = await EventKitService.shared.requestReminderAccess() ? .granted : EventKitService.shared.reminderAccessState }
|
||||
)
|
||||
rowDivider()
|
||||
if !PersonalDataTools.isContactsHiddenPendingAppleFix {
|
||||
personalDataRow(
|
||||
title: "Contacts",
|
||||
isEnabled: $settingsService.contactsEnabled,
|
||||
@@ -883,6 +897,7 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState }
|
||||
)
|
||||
rowDivider()
|
||||
}
|
||||
personalDataRow(
|
||||
title: "Location & Maps",
|
||||
isEnabled: $settingsService.locationMapsEnabled,
|
||||
@@ -2580,37 +2595,6 @@ It's better to admit "I need more information" or "I cannot do that" than to fak
|
||||
}
|
||||
}
|
||||
|
||||
// Automatic Backup
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Automatic Backup")
|
||||
formSection {
|
||||
row("Frequency") {
|
||||
Picker("", selection: $settingsService.autoBackupFrequency) {
|
||||
Text("Off").tag("manual")
|
||||
Text("Daily").tag("daily")
|
||||
Text("Weekly").tag("weekly")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 220)
|
||||
}
|
||||
}
|
||||
Text("When enabled, oAI backs up automatically in the background (checked at launch and hourly while running) — no need to press \"Back Up Now\" yourself.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
// Favorites Sync
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Favorite Models")
|
||||
Text("Starred models sync automatically via the same iCloud Drive folder — star a model on one Mac and it appears starred on your others within a launch or two.")
|
||||
.font(.system(size: 13))
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
|
||||
// Actions
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
sectionHeader("Actions")
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.reminders</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.addressbook</key>
|
||||
<key>com.apple.security.personal-information.contacts</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
|
||||
+8
-19
@@ -43,11 +43,6 @@ struct oAIApp: App {
|
||||
await GitSyncService.shared.syncOnStartup()
|
||||
}
|
||||
|
||||
// Reconcile starred models with the iCloud copy (cross-machine favorites sync)
|
||||
Task {
|
||||
await BackupService.shared.syncFavoritesOnLaunch()
|
||||
}
|
||||
|
||||
// Check for updates in the background
|
||||
UpdateCheckService.shared.checkForUpdates()
|
||||
}
|
||||
@@ -61,11 +56,6 @@ struct oAIApp: App {
|
||||
AboutView()
|
||||
}
|
||||
#if os(macOS)
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
|
||||
Task {
|
||||
await BackupService.shared.syncFavoritesOnLaunch()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in
|
||||
Task { @MainActor in ExternalMCPManager.shared.stopAll() }
|
||||
Task {
|
||||
@@ -126,11 +116,8 @@ struct oAIApp: App {
|
||||
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
|
||||
}
|
||||
|
||||
// ── Chat menu ─────────────────────────────────────────────────
|
||||
// Named "Chat" (not "View") to avoid colliding with the "View" menu
|
||||
// macOS auto-adds for NavigationSplitView (Enter Full Screen, etc.) —
|
||||
// two menus both titled "View" would otherwise appear in the menu bar.
|
||||
CommandMenu("Chat") {
|
||||
// ── View menu ─────────────────────────────────────────────────
|
||||
CommandMenu("View") {
|
||||
Button("Select Model") { chatViewModel.showModelSelector = true }
|
||||
.keyboardShortcut("m", modifiers: .command)
|
||||
|
||||
@@ -145,6 +132,9 @@ struct oAIApp: App {
|
||||
Button("Command History") { chatViewModel.showHistory = true }
|
||||
.keyboardShortcut("h", modifiers: [.command, .shift])
|
||||
|
||||
Button("In-App Help") { chatViewModel.showHelp = true }
|
||||
.keyboardShortcut("/", modifiers: .command)
|
||||
|
||||
Button("Credits") { chatViewModel.showCredits = true }
|
||||
|
||||
Divider()
|
||||
@@ -171,12 +161,11 @@ struct oAIApp: App {
|
||||
|
||||
#if os(macOS)
|
||||
private func openHelp() {
|
||||
// Opens the Help Book's index.html directly in the default browser rather than
|
||||
// through NSHelpManager/Help Viewer — see CLAUDE.md's macOS 27 beta note for why
|
||||
// (Apple's Tips.app replacement for Help Viewer can't resolve anchors on this beta).
|
||||
// Revisit once macOS 27 reaches RC.
|
||||
if let helpBookURL = Bundle.main.url(forResource: "oAI.help", withExtension: nil) {
|
||||
NSWorkspace.shared.open(helpBookURL.appendingPathComponent("Contents/Resources/en.lproj/index.html"))
|
||||
} else {
|
||||
// Fallback to Apple Help if help book not found
|
||||
NSHelpManager.shared.openHelpAnchor("", inBook: Bundle.main.object(forInfoDictionaryKey: "CFBundleHelpBookName") as? String)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user