20 Commits
Author SHA1 Message Date
rune 65fe057aa9 Merge pull request '2.4.2' (#8) from 2.4.2 into main
Reviewed-on: #8
2026-07-22 15:26:48 +02:00
rune 854fd02fad Add live search to the Help Book's table of contents
A search box above the Contents list filters TOC entries by each
linked section's full text content (not just the link title), so
e.g. searching "european" surfaces "Slash Commands" via its command
history date-format note. Pure vanilla JS/CSS, no dependencies --
the page is a static file opened directly in the default browser.

Tested interactively in Safari Technology Preview: filtering,
no-results state, reset, and click-through navigation all verified
working, in both the matched and empty-query cases.
2026-07-22 15:21:30 +02:00
rune e9aceca4e7 Remove now-dead isContactsHiddenPendingAppleFix kill switch
Contacts was never actually broken by an Apple/OS bug -- it was a
wrong entitlement key in oAI.entitlements, now fixed (abf25bd). No
functional change here: the flag was already false in both places
this session, so behavior is identical; this just removes the
now-pointless beta-badge/conditional-row scaffolding built around it.
2026-07-21 15:01:19 +02:00
rune abf25bd897 Fix Contacts TCC entitlement key: contacts -> addressbook
Confirmed root cause via tccd's own log output: the hardened-runtime
hardened-runtime prompting policy checks for
com.apple.security.personal-information.addressbook (the Contacts
framework's TCC service is still internally named kTCCServiceAddressBook,
a holdover from the old AddressBook framework), not
com.apple.security.personal-information.contacts as the entitlements
file had. With the correct key, tccd allows the prompt and access is
now granted correctly on both macOS 27 beta and 26.5.1 stable — this
was never an OS bug, notarization requirement, or beta-only issue.

Also keeps the fuller CNError domain/code/userInfo logging added while
diagnosing this, in case Contacts TCC issues resurface.
2026-07-21 14:59:39 +02:00
rune 77cc646ee0 Re-enable Contacts row to retest on macOS 27 beta 4
isContactsHiddenPendingAppleFix was flipped true on beta 2 after
CNContactStore.requestAccess returned instant "Access Denied" under
hardened runtime, while Calendar/Reminders/Location worked fine.
Rune just installed beta 4 and wants to retest.
2026-07-21 14:16:03 +02:00
rune 57f407ea50 Bump version to 2.4.2 2026-07-21 14:07:22 +02:00
rune 5db1b40552 Union favorites on tied timestamps instead of dropping one side
Two machines that already had favorites before this sync feature
shipped will both have an empty favoriteModelsUpdatedAt, so the
first sync between them ties. Previously that meant the second
machine silently kept only its own set; now tied timestamps merge
via union and re-push, so no pre-existing favorites are lost.
2026-07-21 14:06:05 +02:00
rune fc786d48f8 Sync starred models across machines; add automatic backup scheduling
Favorites now push/pull through a small oai_favorites.json file in the
same iCloud Drive folder used by Settings > Backup, reconciled by
last-write-wins timestamp on launch and app-become-active. Also adds
an Off/Daily/Weekly frequency picker so the full settings backup can
run itself (checked at launch and hourly) instead of requiring a
manual "Back Up Now" click every time.
2026-07-21 13:19:50 +02:00
rune 86027001c7 Roll back native Help Viewer integration; fix duplicate View menu
Per Rune: the NSHelpManager-based Cmd+? fix from the last few commits
technically worked (registration was correct) but opened Apple's
broken generic Tips landing page instead of oAI's own content on the
macOS 27 beta this is built against — worse than the original
browser-tab behavior. Revisit at macOS 27 RC1 (see CLAUDE.md).

- openHelp() reverts to NSWorkspace.open() on index.html directly.
- Removed the "In-App Help" Ctrl+Cmd+H menu item entirely — HelpView's
  panel (search already fully working) is reachable only via /help
  from the input field now, by design.
- Renamed the custom CommandMenu("View") to CommandMenu("Chat") — it
  was colliding with the "View" menu macOS auto-adds for
  NavigationSplitView (Enter Full Screen, etc.), producing two
  identically-titled top-level menus in the menu bar.
2026-07-20 08:17:33 +02:00
rune 98979584fb Fix dead Cmd+/ shortcut for In-App Help; rebind to Ctrl+Cmd+H
Cmd+/ was never reaching the "In-App Help" menu item — AppKit
auto-reserves Cmd+/ to open/focus the app's own Help menu (same
mechanism as Cmd+?), silently pre-empting any custom binding on that
combo, exactly like the earlier Cmd+H (Hide Application) conflict.

Verified live with computer-use before landing on Ctrl+Cmd+H:
Option+Cmd+/ has unpredictable menu-glyph rendering and didn't fire;
Option+Cmd+H is macOS's reserved "Hide Others" shortcut (visibly hid
other app windows when tested). Ctrl+Cmd+H showed no OS-level effect
and correctly opens the panel.

HelpView's search bar was already fully implemented and working —
this was purely a matter of the shortcut never reaching it.
2026-07-20 08:00:25 +02:00
rune 333e288418 Fix Help Book keys missing from the built Info.plist
GENERATE_INFOPLIST_FILE = YES with no INFOPLIST_FILE meant Xcode
synthesized Info.plist purely from INFOPLIST_KEY_* build settings and
silently ignored oAI/Info.plist on disk — so CFBundleHelpBookName/
CFBundleHelpBookFolder never made it into the built app. NSHelpManager
therefore couldn't resolve the book and macOS showed the generic Help
Center instead of oAI's help content (previous commit 74c8be8 fixed
openHelp() to call NSHelpManager, but that call had nothing to find).

Fix: point INFOPLIST_FILE at oAI/Info.plist so Xcode merges its extra
keys into the generated Info.plist, and add a synchronized-group
membership exception so Info.plist isn't also copied into Resources
(would otherwise produce a duplicate-file build warning).
2026-07-20 07:33:51 +02:00
rune 74c8be8f94 Fix Cmd+? opening Help Book in the default browser instead of Help Viewer
openHelp() always found the bundled oAI.help folder, so it always took
the NSWorkspace.open(index.html) branch — handing the page to the
default web browser. The NSHelpManager branch, which launches the
native Help Viewer (with its built-in full-text search), was
unreachable dead code. Now always routes through NSHelpManager.
2026-07-20 07:24:20 +02:00
rune 75e5b40f45 Persist sidebar visibility; add language-matching rule to default prompt
- ContentView now reads/writes SettingsService.sidebarVisible so the
  NavigationSplitView sidebar's shown/hidden state survives relaunch,
  matching the existing window size/position persistence.
- Default system prompt gains a rule: always reply in the user's
  language, even for requests (e.g. translation) targeting another one.
2026-07-19 16:01:04 +02:00
runeandClaude Sonnet 5 6d7f11d705 Clarify that only the latest release is supported for security fixes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 09:07:39 +02:00
runeandClaude Sonnet 5 d38b5442ed Clarify that only the latest release is supported for security fixes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 09:07:37 +02:00
runeandClaude Sonnet 5 e3fae25198 Add SECURITY.md with vulnerability reporting policy
Points reporters to the contact form at oai.pm instead of public issues.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 09:05:25 +02:00
runeandClaude Sonnet 5 b9c3c97dc0 Add SECURITY.md with vulnerability reporting policy
Points reporters to the contact form at oai.pm instead of public issues.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 09:05:18 +02:00
runeandClaude Sonnet 5 bd686873c4 Update commercial licensing contact URL to oai.pm
Consolidates the mac.oai.pm subdomain references (introduced in the
PolyForm Noncommercial relicense) to the root oai.pm domain, across
the LICENSE file, README, and all Swift source file headers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 11:33:53 +02:00
rune e0ab4041a2 Merge PolyForm Noncommercial relicense from 2.4.1 into main 2026-07-15 11:17:58 +02:00
rune 52ab774785 Merge pull request '2.4.1' (#7) from 2.4.1 into main
Reviewed-on: #7
2026-07-14 11:01:35 +02:00
13 changed files with 299 additions and 52 deletions
+29
View File
@@ -0,0 +1,29 @@
# 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.
+17 -2
View File
@@ -15,9 +15,22 @@
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>";
};
@@ -270,6 +283,7 @@
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.";
@@ -289,7 +303,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.1;
MARKETING_VERSION = 2.4.2;
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -319,6 +333,7 @@
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.";
@@ -338,7 +353,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.1;
MARKETING_VERSION = 2.4.2;
PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@@ -20,7 +20,11 @@
<nav class="toc">
<h2>Contents</h2>
<ul>
<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">
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#providers">AI Providers &amp; API Keys</a></li>
<li><a href="#models">Selecting Models</a></li>
@@ -1526,9 +1530,6 @@ 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>
@@ -1815,5 +1816,37 @@ 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,6 +101,32 @@ 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;
}
+94
View File
@@ -34,6 +34,15 @@ 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
@@ -51,6 +60,8 @@ 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",
@@ -71,6 +82,7 @@ final class BackupService {
private init() {
checkForExistingBackup()
startAutoBackupTimer()
}
// MARK: - iCloud Path Resolution
@@ -176,6 +188,88 @@ 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 {
+2 -1
View File
@@ -63,7 +63,8 @@ class ContactsService {
store.requestAccess(for: .contacts) { granted, error in
let after = CNContactStore.authorizationStatus(for: .contacts)
if let error {
Log.mcp.error("ContactsService.requestAccess: error=\(error.localizedDescription); granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
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))")
} else {
Log.mcp.info("ContactsService.requestAccess: granted=\(granted); status after = \(Self.describe(after)) (raw=\(after.rawValue))")
}
+27 -9
View File
@@ -25,16 +25,11 @@ import Foundation
import os
import Security
/// 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.
/// 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.
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
@@ -520,10 +515,33 @@ 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
@@ -695,7 +713,7 @@ class SettingsService {
}
var contactsEnabled: Bool {
get { !PersonalDataTools.isHiddenPendingAppleFix && !PersonalDataTools.isContactsHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
get { !PersonalDataTools.isHiddenPendingAppleFix && cache["contactsEnabled"] == "true" }
set {
cache["contactsEnabled"] = String(newValue)
DatabaseService.shared.setSetting(key: "contactsEnabled", value: String(newValue))
+1
View File
@@ -90,6 +90,7 @@ 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
+5 -1
View File
@@ -29,7 +29,8 @@ import Darwin // uname, sysctlbyname
struct ContentView: View {
@Environment(ChatViewModel.self) var chatViewModel
private var updateService = UpdateCheckService.shared
@State private var columnVisibility: NavigationSplitViewVisibility = .all
@State private var columnVisibility: NavigationSplitViewVisibility =
SettingsService.shared.sidebarVisible ? .all : .detailOnly
@State private var showIntelWarning = false
var body: some View {
@@ -46,6 +47,9 @@ 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 }
-1
View File
@@ -213,7 +213,6 @@ 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
+41 -25
View File
@@ -125,6 +125,7 @@ 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
@@ -828,10 +829,6 @@ 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()
@@ -842,18 +839,8 @@ 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, 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, 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.")
.font(.system(size: 14))
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
@@ -888,16 +875,14 @@ 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,
state: contactsAccessState,
systemSettingsAnchor: "Privacy_Contacts",
requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState }
)
rowDivider()
}
personalDataRow(
title: "Contacts",
isEnabled: $settingsService.contactsEnabled,
state: contactsAccessState,
systemSettingsAnchor: "Privacy_Contacts",
requestAccess: { contactsAccessState = await ContactsService.shared.requestAccess() ? .granted : ContactsService.shared.accessState }
)
rowDivider()
personalDataRow(
title: "Location & Maps",
isEnabled: $settingsService.locationMapsEnabled,
@@ -2595,6 +2580,37 @@ 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")
+1 -1
View File
@@ -12,7 +12,7 @@
<true/>
<key>com.apple.security.personal-information.reminders</key>
<true/>
<key>com.apple.security.personal-information.contacts</key>
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<key>com.apple.security.personal-information.location</key>
<true/>
+19 -8
View File
@@ -43,6 +43,11 @@ 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()
}
@@ -56,6 +61,11 @@ 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 {
@@ -116,8 +126,11 @@ struct oAIApp: App {
.disabled(chatViewModel.messages.filter { $0.role != .system }.isEmpty)
}
// View menu
CommandMenu("View") {
// 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") {
Button("Select Model") { chatViewModel.showModelSelector = true }
.keyboardShortcut("m", modifiers: .command)
@@ -132,9 +145,6 @@ 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()
@@ -161,11 +171,12 @@ 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