Skip to content
Open

V3 #157

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4f704a3
✨ Release 3.0.0: assistant-centric settings, chat inspector, export, …
Renset Jul 7, 2026
3108d69
Prepare v3.0.0 release
Renset Jul 11, 2026
76d0f88
Fix Responses API assistant history encoding
Renset Jul 11, 2026
93a24d7
Fix v3 migration and assistant settings inheritance
Renset Jul 11, 2026
0dbdbf1
Allow local HTTP endpoints (#150)
Renset Jul 11, 2026
181e586
Support pasting images into chat input (#153)
Renset Jul 11, 2026
586ca41
Add custom API service headers (#156)
Renset Jul 11, 2026
317f1a2
Add assistant mention picker (#149)
Renset Jul 12, 2026
66d0092
Refine assistant mention picker (#149)
Renset Jul 12, 2026
4655e04
Improve assistant picker layout and details (#149)
Renset Jul 12, 2026
4bf1606
Fix assistant picker height (#149)
Renset Jul 12, 2026
1e562b2
Position assistant picker above input (#149)
Renset Jul 12, 2026
6c09561
Reject non-image clipboard files (#153)
Renset Jul 12, 2026
063a653
Support capability-aware PDF paste (#153)
Renset Jul 12, 2026
c97d6a0
Refactor API service connection test
Renset Jul 12, 2026
d6a50d3
Move default assistant ownership to assistants
Renset Jul 12, 2026
aec77f9
Add assistant-first chat creation flow
Renset Jul 12, 2026
632a734
Keep API connection button geometry stable
Renset Jul 12, 2026
d336480
Make Advanced disclosure label clickable
Renset Jul 12, 2026
27c788e
Add bulk chat selection and actions
Renset Jul 12, 2026
218df55
Refine bulk chat selection UI
Renset Jul 12, 2026
26df83e
Align bulk action bar with macOS conventions
Renset Jul 12, 2026
86bb9e8
Remove bulk selection toolbar button
Renset Jul 12, 2026
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ DerivedData/
*.Trash
*.xcworkspace/xcuserdata/*/UserInterfaceState.xcuserstate
macai.xcodeproj/project.xcworkspace/xcuserdata/*/UserInterfaceState.xcuserstate
code-review/*

# Local working notes (not part of the public repo)
temp/
Assistant_changes.md
CODE_REVIEW.md
CoreData_FIX_Plan.md
GEMINI_SDK_DETAILS.md
IOS_IMPLEMENTATION_PLAN.md
95 changes: 95 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Draft-Save Crash Fix (Core Data)

## Summary
We fixed EXC_BAD_ACCESS / Core Data crashes tied to draft saving and iCloud sync. The primary issue was a Core Data attribute named **`newMessage`**, which conflicts with Objective‑C “new*” naming rules and can crash during Swift/Obj‑C bridging. Secondary fixes ensured Core Data access stays on the correct queue.

## Root Cause
1) **Core Data attribute named `newMessage`**. Properties starting with `new*` are treated specially by Objective‑C (“Create Rule”). Core Data’s dynamic accessors and Swift bridging can crash when a model attribute uses a `new*` name.
2) Crashes appeared during SwiftUI updates and iCloud sync (forwarding / bridging failures such as `___forwarding___` + Swift string bridging).
3) Some Core Data fetches were performed off the context’s queue (e.g., in async tasks), triggering `_PFAssertSafeMultiThreadedAccess_impl`.

## Fix Strategy
### A) Rename the Core Data draft field
`ChatEntity.newMessage` → `ChatEntity.draftMessage` (with `renamingIdentifier="newMessage"`).
This avoids Objective‑C “new*” naming collisions while preserving data during migration and sync.

### B) Isolate draft saves in a background context
Drafts are saved in a private queue context with a unique `transactionAuthor`:
- `AppConstants.draftTransactionAuthor = "Drafts"`
- Draft saves use this context only.

### C) Store only IDs in drafts
Drafts store message text + attachment **IDs**, not entire objects. This keeps draft saves lightweight and avoids merging attachment data into the UI context.

### D) Fix Core Data queue violations
Every Core Data fetch from background threads now uses `perform` / `performAndWait` on the correct context.

## Key Code Changes
### Draft handling
- `macai/UI/Chat/ChatDraftManager.swift`
- Uses private background context for draft saves.
- Drafts store `draftImageIDs` / `draftFileIDs` only.
- Draft restore fetches IDs and loads attachments by ID.
- Draft message reads use `draftMessage` and detach the Swift string from Core Data objects.

- `macai/Configuration/AppConstants.swift`
- Added `draftTransactionAuthor`.

### Data model rename
- `macai/Store/macaiDataModel.xcdatamodeld/.../contents`
- `newMessage` renamed to `draftMessage` with `renamingIdentifier="newMessage"`.

### Core Data save reliability
- `macai/Utilities/Extensions.swift`
- `saveWithRetryOnQueue` ensures save happens on context queue.

### Thread-safety fixes (Core Data fetches)
These now use `performAndWait` on `viewContext`:
- `macai/Utilities/APIHandlers/OpenAIHandlerBase.swift`
- `macai/Utilities/APIHandlers/GeminiHandler.swift`
- `macai/Utilities/MessageParser.swift`

These now fetch entity data **from the entity’s context queue** before using it on background threads:
- `macai/Models/ImageAttachment.swift`
- `macai/Models/DocumentAttachment.swift`

### Attachment persistence for drafts
Attachments added via Photos picker or drag/drop are persisted immediately so the draft can restore them later:
- `macai/UI/Chat/BottomContainer/MessageInputView.swift`

### PDF attachments (general)
- PDFs are represented in message text as `<file-uuid>...</file-uuid>` placeholders and stored as `DocumentEntity` records with `fileData`, `filename`, and `mimeType`.
- The UI exposes “Add PDF” and PDF previews when `pdfUploadsAllowed` is enabled for the selected API service.
- Drafts store only PDF IDs (`draftFileIDs`) and restore PDFs by ID.

### OpenAI PDF attachments
OpenAI Responses models accept PDFs via `input_file` items (`file_data` data URL + `filename`) in the request payload:
- `macai/Utilities/APIHandlers/OpenAIResponsesHandler.swift`
- `macai/Utilities/APIHandlers/OpenAIHandlerBase.swift`
- `macai/UI/Chat/ChatLogicHandler.swift`
- `macai/UI/Chat/ChatView.swift`

### Gemini PDF attachments
Gemini now supports PDF attachments by translating `<file-uuid>` placeholders into `inlineData` parts (base64 + mime type), and PDF uploads are enabled in Gemini defaults/templates:
- `macai/Utilities/APIHandlers/GeminiHandler.swift`
- `macai/Configuration/AppConstants.swift`
- `macai/Utilities/APIServiceTemplateProvider.swift`
- `macai/Configuration/APIServiceTemplates.json`
- Existing Gemini services are backfilled via `DatabasePatcher.patchGeminiPdfUploadsForAPIServices` (flag: `geminiPdfUploadsPatchCompletedKey`).

## Context Primer (Short Version)
- A **context** is a Core Data workspace with its own thread/queue.
- Objects from a context must only be accessed on that context’s queue.
- The **viewContext** is tied to the main thread (SwiftUI).
- Background contexts are used for async work (e.g., draft autosaves).
- If you read or write objects on the wrong queue, Core Data throws `_PFAssertSafeMultiThreadedAccess_impl`.

## Testing Checklist
1) Type text → switch chat → switch back (draft restores, no crash).
2) Add image/PDF → switch chat → switch back (draft restores, no crash).
3) Send message with attachments (no crash).
4) Add attachment via drag/drop and Photos picker (draft restores).

## Notes / Caveats
- Draft data is restored explicitly via `ChatDraftManager.draftSnapshot(...)` during `ChatView` init.
- If you introduce new Core Data fetches that run off-main, wrap them in `context.perform {}` or `performAndWait {}`.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ Run macai with Ollama is easy:
6. Test and enjoy!

## System requirements
macOS 14.0 and later (both Intel and Apple chips are supported)
macOS 15.0 and later (both Intel and Apple chips are supported)

## Project status
Project is in the active development phase.

## Disclaimer
This software is provided "as is", without warranty of any kind. Use at your own risk.

### Build from source

#### Option 1: With Apple Developer Account (Full Features)
Expand Down
164 changes: 164 additions & 0 deletions docs/PRD_CoreData_V3_Migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# PRD: CoreData Model V3 Migration

## Overview

This document describes the CoreData model migration from v1 (base model) to v3, which was implemented to support CloudKit synchronization and improve message ordering performance.

## Background

The original CoreData model used an **ordered relationship** (`ordered="YES"`) for the `ChatEntity.messages` relationship. This approach had two limitations:

1. **CloudKit Incompatibility**: CloudKit does not support ordered relationships
2. **Performance**: Ordered relationships can be slower for large datasets

## Changes Summary

### Model Changes (v1 → v3)

| Entity | Attribute/Relationship | v1 (Base) | v3 |
|--------|------------------------|-----------|-----|
| ChatEntity | messages | `ordered="YES"` | Unordered (no `ordered` attribute) |
| ChatEntity | lastSequence | Not present | `Int64`, non-optional, default: 0 |
| MessageEntity | sequence | Not present | `Int64`, non-optional, default: 0 |
| ChatEntity | apiService | No inverse | Has inverse: `chats` |
| ChatEntity | persona | No inverse | Has inverse: `chats` |
| APIServiceEntity | chats | Not present | To-many relationship (inverse of apiService) |
| APIServiceEntity | defaultPersona | No inverse | Has inverse: `apiServicesAsDefault` |
| PersonaEntity | chats | Not present | To-many relationship (inverse of persona) |
| PersonaEntity | apiServicesAsDefault | Not present | To-many relationship (inverse of defaultPersona) |

### Key Technical Decisions

#### 1. Custom Mapping Model Required

The change from `ordered="YES"` to unordered relationship is **not supported by lightweight migration**. This required creating a custom mapping model:

- **File**: `V1toV3.xcmappingmodel` (created via Xcode)
- **Source Model**: `macaiDataModel.xcdatamodel`
- **Destination Model**: `macaiDataModel 3.xcdatamodel`

#### 2. Migration Configuration

```swift
// In PersistenceController.init()
for description in container.persistentStoreDescriptions {
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = false // Use custom mapping model
}
```

#### 3. Message Ordering Strategy

Instead of relying on CoreData's ordered relationship, messages are now ordered using:

1. **`sequence` attribute**: Each message gets a sequence number within its chat
2. **`lastSequence` attribute**: Each chat tracks the last assigned sequence number
3. **Fetch with sort descriptors**: Messages are fetched and sorted by sequence

```swift
// In ChatEntity.messagesArray
let fetchRequest = NSFetchRequest<MessageEntity>(entityName: "MessageEntity")
fetchRequest.predicate = NSPredicate(format: "chat == %@", self)
fetchRequest.sortDescriptors = [
NSSortDescriptor(keyPath: \MessageEntity.sequence, ascending: true),
NSSortDescriptor(keyPath: \MessageEntity.timestamp, ascending: true),
]
```

#### 4. Sequence Backfill

The `DatabasePatcher.backfillMessageSequencesIfNeeded()` function populates sequence values for existing messages after migration, sorting by timestamp.

## File Structure

```
macai/Store/
├── macaiDataModel.xcdatamodeld/
│ ├── .xccurrentversion # Points to "macaiDataModel 3.xcdatamodel"
│ ├── macaiDataModel.xcdatamodel # v1 - Base model (ordered messages)
│ ├── macaiDataModel 2.xcdatamodel # v2 - Intermediate (unused for this migration)
│ └── macaiDataModel 3.xcdatamodel # v3 - Target model (unordered + sequence)
├── V1toV3.xcmappingmodel # Custom mapping model
└── ChatStore.swift
```

## Backup Strategy

### Automatic Pre-Migration Backup

Before migration, the app automatically creates a backup of the CoreData files:

- **Location**: `~/Library/Application Support/<bundleID>/Backups/Before-macaiDataModel 3-<timestamp>/`
- **Files backed up**: `.sqlite`, `.sqlite-wal`, `.sqlite-shm`
- **One-time flag**: `CoreDataBackupBeforeV3Completed` in UserDefaults

### Backup Discovery

Backups are discoverable in Settings → Backup & Restore, implemented via `DatabaseBackupManager.discoverBackups()`.

## Migration Flow

```
App Launch
│
▼
CoreDataBackupManager.backupBeforeV3IfNeeded()
│ (Creates backup if flag not set)
▼
container.loadPersistentStores()
│ (Uses V1toV3.xcmappingmodel for migration)
▼
DatabasePatcher.applyPatches()
│
├── patchMissingIDs()
├── backfillMessageSequencesIfNeeded()
└── (other patches...)
│
▼
App Ready
```

## Testing Checklist

- [ ] Fresh install works correctly
- [ ] Migration from v1 database preserves all chats and messages
- [ ] Messages appear in correct order after migration
- [ ] Backup is created before migration
- [ ] Backup appears in Settings → Backup & Restore
- [ ] CloudKit sync works with new model (if enabled)

## Troubleshooting

### Migration Fails / Empty App After Update

1. Check console for "Failed to load persistent store" errors
2. Verify `.xccurrentversion` points to `macaiDataModel 3.xcdatamodel`
3. Ensure `V1toV3.xcmappingmodel` is included in the target
4. Verify `shouldInferMappingModelAutomatically = false`

### Backup Not Created

1. Check if `CoreDataBackupBeforeV3Completed` flag is already set:
```bash
defaults read com.notfullin.macai CoreDataBackupBeforeV3Completed
```
2. Reset flag to test:
```bash
defaults delete com.notfullin.macai CoreDataBackupBeforeV3Completed
```

### Messages Out of Order

1. Check if `backfillMessageSequencesIfNeeded()` ran successfully
2. Verify `MessageSequenceBackfillCompleted` flag in UserDefaults
3. Reset and re-run if needed:
```bash
defaults delete com.notfullin.macai MessageSequenceBackfillCompleted
```

## Related Files

- `macai/macaiApp.swift` - PersistenceController, CoreDataBackupManager
- `macai/Models/Models.swift` - ChatEntity, MessageEntity with sequence logic
- `macai/Utilities/DatabasePatcher.swift` - Migration patches
- `macai/UI/Preferences/TabBackupRestoreView.swift` - Backup UI
Loading
Loading