New "Read Release Notes" entries in the Help menu (current installed
version) and the "Check for Updates" alert (the new, not-yet-installed
version) render a release's markdown notes in a Confab modal.
- UpdateCheckService.fetchReleaseNotes(forTag:) fetches a release's
title + body from Gitea's public releases-by-tag API, caching the
result by version tag in the settings table (a published release's
notes don't change, so no need to refetch on every view).
- ReleaseNotesView reuses the existing MarkdownContentView renderer;
shows a friendly "not available yet" state for versions with no
published Gitea release (e.g. a dev build ahead of the last release).
- ReleaseNotesRequest carries which version to show atomically via
.sheet(item:), per this project's established sheet-timing pattern.
- Removed the redundant "Release Page" button from the update alert
now that notes show in-app; added an explicit .keyboardShortcut
(.cancelAction) to its cancel button so Escape actually closes it —
role: .cancel alone didn't do it, since NSAlert only auto-binds
Escape to a button literally titled "Cancel".
Folders and conversation→folder assignments now sync across machines:
- Folder gains updatedAt (v11 migration) to resolve renames/reparents
last-write-wins across machines.
- New folders.json manifest at the sync repo root: folder tree +
conversationId→folderId assignments, imported before conversation
files so new conversations land in the right folder immediately.
- Local folders missing from the manifest are pruned (reparent-safe),
guarded the same way conversation-orphan cleanup already is against
an empty/stale manifest wiping everything.
Three real bugs found and fixed during live multi-machine testing:
- Sidebar never refreshed after Git Sync imported conversations/folders
directly into the database — only reloaded on launch or when the
advanced conversation list closed, with no equivalent hook for the
Settings sheet.
- "Sync Now" exported before pulling, so it could write folders.json
as an untracked file that then collided with the remote's tracked
copy on the next pull ("untracked working tree files would be
overwritten by merge"). Reordered to pull → import → export → push.
- Folder assignment only applied to brand-new conversations during
import, so any conversation already synced to a machine before this
feature existed never got filed — which in practice is every
conversation on a second machine, not an edge case. Now backfills
a folder assignment for existing conversations that aren't filed
anywhere locally yet, without clobbering an already-set folderId.
Also renamed the "Initialize Repository" button to "Clone Repository"
(it's always been a git clone, not new-repo creation) across the UI,
localization catalog, and Help Book.
Both files diverged from a shared base: main got doc content fixes
without the oAI→Confab rename, while 2.5.0 got the same fixes plus the
rename plus new feature docs (crash recovery, unsaved-changes prompt).
2.5.0's version is a strict superset, so conflicts resolved in its favor.
Folders can now contain other folders, arbitrarily deep — e.g. "Work"
containing "Project A"/"Project B". v10 migration adds a
self-referencing parentId column; tree ordering, depth, and cycle
detection are pure Swift (Folder.orderedTree/isDescendant/
visibleFolderIds), not SQL, so listFolders() stays a simple flat
query.
- Create nested folders via "New Subfolder…" (context menu, both
list views) or by dragging a folder onto another to reparent it.
Dragging onto an existing descendant is rejected (cycle guard).
- Deleting a folder reparents its children and any conversations
filed directly in it up one level to the deleted folder's own
parent — conversations are never deleted. This also fixes a real
bug: the previous deleteFolder never persisted unfiling to the
database, only patched in-memory state, so a conversation whose
folder was deleted kept a dangling folderId and silently vanished
from view after the next relaunch.
- All "Move to Folder" pickers (sidebar, advanced list, per-row
context menus, the Save dialog's folder popup) show an indented
flat list reflecting the tree.
- New DraggedItem enum disambiguates a dragged folder from dragged
conversation(s) in the shared string-based drag payload, and
unifies both list views on the same bundled-multi-selection format
— closes a gap where dragging a multi-selection in the advanced
list (⌘L) only moved the one row grabbed, unlike the sidebar.
Confirmed working live, including relaunch-survival of the
delete/reparent fix.
ConversationListView (advanced list, ⌘L): ⌘-click toggles a row,
Shift-click selects a contiguous range, and a "Move to Folder"
toolbar button/context-menu entry moves every selected conversation
at once. Confirmed working live.
SidebarView: same capability, adapted to the sidebar's own click
model since opening a chat there previously required only a single
click. Single-click now selects only (replacing the prior selection),
⌘/Shift-click work the same as the advanced list, and double-click
opens a chat (clearing the selection). Selected rows get a distinct
neutral tint from the existing accent highlight used for the
currently-open conversation. Dragging a row that's part of a
multi-selection now bundles every selected conversation's ID into the
drag payload, so dropping on a folder moves the whole selection
instead of just the dragged row.
Range-selection math (idsInRange) is defined once on
ConversationListView and reused directly by SidebarView rather than
duplicated — it's `internal`, not `private`, specifically so both
views can share it.
Inline single-backtick spans and multi-line fenced ```blocks``` now
render with monospace styling as you type, plus real per-language
syntax highlighting for fenced blocks (reusing the existing
SyntaxHighlighter utility). Only complete, closed spans/fences light
up — an unterminated backtick or fence is left as plain text until
closed.
Pure regex/range logic extracted into testable static functions
(inlineCodeRanges, fencedCodeBlocks, fencedCodeBlockRanges) rather
than living inline in the NSTextView coordinator.
A full rebrand (oAI -> Confab) warrants a minor version bump rather
than a patch release. Branch renamed from 2.4.4 to 2.5.0 to match the
project's per-version branch convention (old branch deleted from the
remote after the new one was pushed and tracked).
Untrack ConversationListViewPureLogicTests.swift and
NativeTextEditorPureLogicTests.swift — these belong to two other
unrelated, unfinished features (conversation multi-select and input
code formatting) and got swept in by an overly broad glob in the
previous commit. Files remain on disk, just untracked again.
Also stage the deletion of the old oAI.entitlements path — the
earlier git mv to Confab.entitlements had its staged rename undone by
an intermediate git reset, so the previous commit added the new file
without ever removing the old one from tracking.
"oAI" reads as easily confused with OpenAI, both visually and in
casual conversation. Renamed to "Confab" throughout: Xcode
target/scheme/bundle ID (com.oai.Confab), Info.plist and Help Book
identity, all user-facing UI text, internal Log subsystem and color
identifiers, localization catalogs (6 languages, including a proper
reworded/retranslated Intel-deprecation notice), Help Book HTML
content, and docs (README/DEVELOPMENT/PRIVACY/SECURITY).
Deliberately cosmetic-only: the on-disk data folder
(~/Library/Application Support/oAI/), database/backup filenames,
Keychain service identifiers, and EncryptionService's key-derivation
inputs are all left untouched so existing conversations, settings,
and stored API keys survive the update with zero migration and no
re-entering credentials. Verified live: a real signed build
successfully decrypted a stored API key and loaded an existing
conversation database after the bundle ID change.
Also includes a small already-completed, previously uncommitted
model-release-date feature (ModelInfo/OpenRouterModels/
OpenRouterProvider/ModelInfoView) that happened to share several
files with this rename.
Gitignored on this branch and updated on disk but not part of this
commit: CLAUDE.md, RELEASE_NOTES.md, and the build*.sh scripts.
exportAllConversations()'s orphan-cleanup treated "zero local
conversations" as "every synced conversation was deleted," so a
just-cloned repo on a new machine could get emptied and pushed before
the post-clone import ever ran. orphanedExportFilenames() now returns
no orphans when the local ID set is empty, and cloneRepository()
imports immediately after cloning to close the window entirely.
Replaces heuristic auto-save (goodbye-phrase detection, idle timeout,
min-message count, on-model-switch) with a standard macOS unsaved-changes
gate (Save/Don't Save/Cancel) on New Chat, Clear Chat, Load Conversation,
and Quit. The Save dialog gained a folder picker with inline "New Folder…"
creation.
Separately, the in-progress conversation is periodically mirrored to disk
(DraftRecoveryService, configurable interval in Settings, default 10s) and
offered back on next launch if oAI crashes or is force-quit, including the
model that was selected.
Two real bugs found via ObjectIdentifier/log-based diagnosis before this
worked correctly:
- oAIApp.init() wired AppDelegate.chatViewModel from its own @State read,
which returned a throwaway ChatViewModel instance distinct from the one
ContentView actually renders. Wiring moved to ContentView.onAppear.
- NSApplication.shared.delegate as? AppDelegate always failed silently:
@NSApplicationDelegateAdaptor registers an internal SwiftUI.AppDelegate
wrapper as the real NSApp.delegate (same name, different type in a
different module), which forwards protocol methods but isn't castable
to our type. AppDelegate now tracks itself via a static `shared`.
Also guards checkForCrashRecoveryDraft() against running under
XCTestConfigurationFilePath — oAITests is app-hosted, so xcodebuild test
launches this same app, and a leftover draft file on disk would otherwise
hang the entire test run on a blocking NSAlert with no one to click it.
Two things were fighting the previous dark-PDF attempt, both
confirmed by direct experimentation:
- prefers-color-scheme is ignored entirely by the print pipeline —
identical CSS printed light even with the webview's appearance
forced to dark. Screen dark-mode media queries just don't apply to
NSPrintOperation rendering.
- Even with dark colors set unconditionally (no media query), the
print pipeline still dropped every background color and printed
white — browsers/WebKit strip background-color/background-image by
default when printing, to save ink, unless told otherwise via
print-color-adjust: exact.
PDF now has its own always-dark stylesheet (pdfCss) instead of the
prefers-color-scheme-driven one HTML export still uses, plus
`* { print-color-adjust: exact }` so the dark backgrounds actually
survive the print pass. html(name:messages:) takes a new
forceDarkCSS parameter (default false, unchanged for HTML/other
callers); pdfData passes true. Dropped the now-pointless
webView.appearance forcing, since dark is unconditional now.
Verified with the same standalone repro-script + Read-the-PDF method:
dark page background, colored message boxes, dark code block, all
correctly surviving the real A4 print pipeline.
Rune compared our PDF against a normal reference PDF and the
difference was structural, not just a font-size tweak: the reference
was 6 standard A4 pages (595x842pt); ours was ONE continuous page
850x6886pt — nearly 8 feet tall. That's what createPDF() actually
does when content overflows its frame (confirmed in the prior commit)
— it auto-grows to fit everything as a single non-standard-sized
page, never paginating. There was no page of a familiar size to judge
the "normal" font size against, which is what actually read as "huge"
even after the sizing fixes.
Replaced with WKWebView's real print pipeline: build an NSPrintInfo
for US Letter (612x792pt) with normal margins, get a print operation
via webView.printOperation(with:), and run it silently (no panel) to
a temp file, dispatched off the main actor since NSPrintOperation.run()
blocks synchronously. This is genuine multi-page pagination — same
mechanism any app's real print-to-PDF uses.
Also added break-inside/page-break-inside: avoid on .message so a
single message doesn't get split awkwardly across a page boundary.
Verified with a standalone reproduction script (same method as the
prior fix) using the actual exported CSS and realistic multi-message
content: clean 2-page US Letter output, message boundaries respected
across the page break, normal-looking document proportions.
The remaining "text still looks large" complaint after the page-
geometry fix was real: .message h1-h6 only ever set margin, never
font-size, so any markdown header inside an AI response (very common
in formatted answers — "## Reality Check on Capacity" etc) fell back
to the browser's default heading sizes (h2 ≈ 22px+ against a 15px
body). Gave headers an explicit, compact scale (19/17/15.5/14px) and
trimmed the base body size from 15px to 14px and the outer title from
24px to 22px for a tighter, more document-like feel overall.
Verified by reproducing the exact content from the reported screenshot
(the M1 AI Setup conversation with its "Reality Check on Capacity"
h2 and bolded list items) through the same standalone WKWebView/
createPDF harness used for the previous fix, and visually confirming
the heading now renders proportionately instead of oversized.
Empirically reproduced this outside the app (standalone WKWebView +
createPDF script, inspecting real PDF page bounds via PDFKit) instead
of guessing again. Findings:
- WKPDFConfiguration.rect left at default captures the webview's
frame size verbatim when content fits within it, and auto-grows to
a single tall page matching full scrollable content when it
overflows. It does not paginate, and setting rect explicitly just
clips to that one rect instead.
- The scrollHeight-based frame resize added last round (to "fix" this
same bug) was itself the problem: resizing the frame before calling
createPDF produced a page that didn't match the resized frame at
all (e.g. resized to 816x295, captured page came out 799x375) — a
small, badly-proportioned page that made ordinarily-sized text look
enormous relative to it. Removed entirely.
- A visible scrollbar during capture shaves ~17pt off the captured
page width. Added ::-webkit-scrollbar { display: none } to prevent
that.
Verified against a 12-message realistic conversation: clean single
page, correct proportions, readable text, matches the intended CSS
layout. The text-size-adjust/viewport-meta fix from last round turned
out to be unnecessary (computed font-size was correct all along) but
is harmless, so left in place.
Three defensive fixes for WKWebView.createPDF() rendering text far
larger than the source HTML's actual font sizes specify:
- Added a viewport meta tag and explicit -webkit-text-size-adjust:
100% — WebKit's shared engine has a text-autosizing heuristic
(normally associated with mobile Safari, but can trigger in WKWebView
contexts too, especially for content loaded via loadHTMLString with
no base URL) that boosts font sizes for perceived readability; this
disables it explicitly rather than relying on it not firing.
- Explicit body font-size (was previously unset, relying on WebKit's
default) and webView.pageZoom = 1.0.
- Resize the webview to the actual rendered content height
(via document.body.scrollHeight) before calling createPDF, instead
of relying on its undocumented auto-sizing against the arbitrary
initial frame — a bounds mismatch here is a known cause of
blown-up/rescaled PDF output.
Previously always silently wrote to Downloads with a fixed name — no
way to pick where it goes or rename it. Added
exportConversationWithSavePanel(format:defaultFilename:), which shows
a native NSSavePanel (defaulting to Downloads + the same filename as
before) before writing. Wired into all three File menu export buttons.
The /export slash command and the conversation list's per-row Export
submenu are unchanged (still write straight to Downloads) — the slash
command already takes an explicit filename argument inline, and the
row export is meant as a quick one-click action rather than a
save-as workflow.
Dark: added a prefers-color-scheme: dark CSS block, so HTML export
adapts to the browser/OS theme it's later viewed in. PDF is baked at
export time so it can't respond live — instead the offscreen WKWebView
used for PDF rendering has its appearance explicitly set to
NSApp.effectiveAppearance, matching whatever mode the app is in right
now at export time.
Tables: the renderer had no table support at all (previously
documented as an intentional scope cut), so GFM pipe tables were
falling through to the plain-paragraph path and showing up as literal
"| --- | --- |" text. Added detection (a row line immediately followed
by a valid dashes/colons separator row) plus alignment parsing from
the separator's colons.
Top item on the roadmap ranking from 2026-07-27 — multi-modal export
alongside the existing Markdown/JSON. New ConversationExportService
consolidates the two previously-duplicated Markdown builders
(ChatViewModel and ConversationListView had separate copies of the
same **User**/**Assistant** + --- format) and adds:
- A hand-rolled Markdown->HTML renderer scoped to what actually shows
up in chat messages (headers, bold/italic, inline code, fenced code
blocks, lists, blockquotes, links, horizontal rules) rather than
full CommonMark/GFM — no existing markdown-to-HTML utility existed
in the codebase, and swift-markdown-ui is SwiftUI-view-only with no
HTML-string export API. Content is HTML-escaped before any markdown
substitution so example code containing "<div>" etc renders as
visible text, not live markup.
- PDF via an offscreen WKWebView loading that same HTML and calling
the official createPDF(configuration:) API (macOS 11+) — no new
project/framework linkage needed, WebKit is a system framework.
Wired into every place Markdown export already existed: File menu
(Export as HTML.../PDF...), /export slash command (now md|html|pdf|json),
and a new Export submenu (Markdown/HTML/PDF) on each conversation row's
context menu in the advanced conversation list, replacing the old
single-format swipe-only export. Help docs and InputBar autocomplete
updated to match.
Creating/renaming/deleting a folder in ConversationListView only ever
updated its own local @State — the sidebar has a separate folders
array that was only refreshed by onAppear or conversation-change
triggers, so a folder created in the modal didn't show up in the
sidebar until the next app launch. Sidebar now reloads when
showConversations transitions to false (the modal closing).
collapse state, make merge provider picker visibly clickable
Deleted conversations coming back: exportAllConversations() only ever
wrote files for conversations that currently exist — it never removed
the exported markdown file for a conversation that had been deleted
locally. That file just sits in the sync repo forever, so every
future pull+import (including on every app startup) silently
resurrects it, since importAllConversations() only skips an import
when a matching local ID already exists. Fixed by having export also
delete orphaned files (conversation ID no longer present locally), and
added GitSyncService.syncAfterDeletion() — a debounced export+push
triggered right after any delete/bulk-delete/merge-cleanup, so the
removal reaches the remote promptly instead of waiting on an
unrelated future auto-save. Existing duplicates need one more manual
delete to clear, but they'll stay gone after that.
Folder collapse state now persists (SettingsService.collapsedFolderIds,
JSON-encoded like favoriteModelIds) and is restored on app launch, in
both the sidebar and the advanced conversation list.
Merge model picker: the provider switcher was legitimate (it does load
each provider's own catalog independently) but looked like plain
text — no chevron, no button styling — so it wasn't obviously
clickable. Restyled to match HeaderView's provider menu affordance
(icon + label + chevron on a colored pill).
The merge model picker was reusing chatViewModel.availableModels,
which only ever holds whichever provider the main chat window
currently has active — fine for the main chat's own switcher (where
provider and model change together via the header), wrong for an
independent one-off picker like this. If your active chat was on
Anthropic, that's all you could pick from here regardless of what
other providers you have configured.
Added its own provider menu (mirroring HeaderView's) and an
independent model list fetched via ProviderRegistry for whichever
provider is selected, so OpenRouter, Anthropic, OpenAI, etc. are all
genuinely selectable regardless of what the main chat is doing.
Folders now list alphabetically (case-insensitive) everywhere they're
shown — sidebar, advanced conversation list, and the "Move to Folder"
menu — rather than creation order. listFolders() does the sort at the
DB layer; in-memory folder arrays are re-sorted after local
create/rename so newly added or renamed folders don't fall out of
order until the next reload.
Combine Conversations' AI-Assisted Merge no longer silently assumes
settings.defaultModel — added a model picker (reusing ModelSelectorView,
defaulting to the current default model/provider) so users can pick
which model performs the merge per-combine. ConversationMergeService.merge
takes optional mergeModelId/mergeProvider overrides.
Rune retested after the previous merge fix (bigger token budget,
stronger instruction) with the exact same DDNS Docker debugging
conversations and got the identical failure — same error text,
same models. Logs showed a single ~86s request with a large prompt
(31k tokens cached) and no truncation signal available to check.
The remaining suspect: the transcript was formatted as "**User:**" /
"**Assistant:**" markdown, which closely mimics a live chat turn
format. Over a long, noisy transcript that includes something reading
like a directive ("no more editing", etc), the model can lose track of
"this is data to merge" and slip into continuing/replying to it
instead — matching exactly what was observed. Replaced the transcript
markers with synthetic, non-chat-like tokens (<<<USER_TURN>>> etc) and
added an explicit "this is not a live conversation" framing both
before and after the transcript block, not just once at the top.
Also: log OpenRouter's finishReason when it's "length" (i.e. the
response was actually cut off) so a future failure like this is
distinguishable from a formatting/instruction-following miss without
guessing from the log lines Rune already has available.
Three changes, all aimed at the same failure: merges of large
conversations (e.g. long debugging sessions) were reliably failing to
parse on both Haiku 4.5 and GLM 5.2.
- maxTokens was a flat 4000 regardless of input size — a merge of two
long conversations needs a much larger completion budget than that,
so the model's JSON array output was getting cut off mid-generation.
Now scaled with transcript size (8000-16000).
- Strengthened the prompt: explicitly tell the model not to respond to
or continue anything found inside the transcripts (a real observed
failure mode was the model echoing/continuing transcript content
instead of merging it), and to emit nothing but the JSON array.
- parseTurns now falls back to scanning for a bracket-balanced JSON
array anywhere in the response (respecting quoted strings) if the
model still wraps the array in commentary despite instructions not
to, instead of failing outright on the first non-JSON response.
.draggable()'s default drag-preview snapshot wasn't inheriting the
app's dark appearance, rendering the conversation name in black on a
transparent background while dragging. Supply an explicit preview
view instead — white text on the accent color pill — so it stays
legible for the duration of the drag, independent of the row's own
(already-correct) rendering before and after the move.
Pin conversation row title text to .foregroundStyle(.primary) instead
of the implicit default — under List's sidebar/source-list style,
AppKit's row highlighting could resolve default text to black on a
just-interacted-with row (e.g. right after a drag/context-menu move),
making it unreadable. Also drop Section(isExpanded:), which was
rendering its own native disclosure chevron on the right in addition
to the custom one already in the header on the left; folder collapse
now works purely off the existing collapsedFolders state with a plain
Section, so only the intended left-side chevron remains.
SwiftUI's dropDestination(for:action:) action closure is Void-
returning, not Bool, so handleDrop's Bool return was being silently
discarded — made the discard explicit with _ =.
Folder sections now collapse via an explicit chevron/tap header
(rather than relying on platform-dependent native disclosure, which
didn't render/click reliably) — same collapsed-state binding also
backs Section(isExpanded:) so content visibility stays in sync.
Conversations are draggable onto folder/Unfiled headers to file/unfile
them. Folder names render bold. "New Folder" is now a dedicated button
next to "New Chat" in the sidebar, and next to "Select" in the
advanced conversation list, instead of a small icon buried in the
search row.
UsageStats/ModelUsageStat/ConversationUsageStat need explicit
nonisolated inits under this project's SWIFT_DEFAULT_ACTOR_ISOLATION =
MainActor setting, same as Message/Conversation. deleteFolder also
discarded FolderRecord.deleteOne's Bool result implicitly via the
write closure's return value.
Conversations can now be filed into flat (non-nested) folders, shown
as collapsible sections in both the sidebar and the advanced
conversation list. New folders migration (v9) adds a folders table
and conversations.folderId with ON DELETE SET NULL, so deleting a
folder unfiles its conversations rather than losing them. Move/rename/
delete via context menu; conversation lists with no folders fall back
to the existing flat view unchanged.
Extends the existing Stats sheet (⌘⇧S) with a Session/All-Time segmented
picker. All-Time aggregates tokens, cost, and message counts across every
saved conversation, broken down by model and by conversation, using the
modelId already stored per message — no schema change needed.
Real errors found and fixed:
- Disclaimer referenced a nonexistent Telegram feature and called the
app "oAI-Web" (leftover from a different project's README)
- Git Sync goodbye-phrase example listed "thanks" as a trigger, which
was deliberately excluded from the actual phrase list (fires on
every polite message)
- UI/UX section still described status pills (MCP/Online/Sync) as
living in the header; they moved to the footer in the v2.4 sidebar
redesign
Missing shipped features added: Apple Intelligence provider, Personal
Data Tools (Calendar/Reminders/Contacts/Location), Research Agents,
External MCP Servers, MCP PDF text extraction, and the /shortcuts,
/skills, /jarvis, // slash commands.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real errors found and fixed:
- Disclaimer referenced a nonexistent Telegram feature and called the
app "oAI-Web" (leftover from a different project's README)
- Git Sync goodbye-phrase example listed "thanks" as a trigger, which
was deliberately excluded from the actual phrase list (fires on
every polite message)
- UI/UX section still described status pills (MCP/Online/Sync) as
living in the header; they moved to the footer in the v2.4 sidebar
redesign
Missing shipped features added: Apple Intelligence provider, Personal
Data Tools (Calendar/Reminders/Contacts/Location), Research Agents,
External MCP Servers, MCP PDF text extraction, and the /shortcuts,
/skills, /jarvis, // slash commands.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DMGs are notarized and stapled automatically now, so the workaround
steps no longer apply. Also corrected the minimum macOS requirement
to match the actual deployment target (26.2, not 14.0).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
DMGs are notarized and stapled automatically now, so the workaround
steps no longer apply. Also corrected the minimum macOS requirement
to match the actual deployment target (26.2, not 14.0).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the old 4 screenshots with 7 current ones covering chat,
General/Apple Intelligence settings, MCP + Personal Data settings,
Agent Skills (list + edit), Advanced settings, and iCloud Backup —
placed next to the sections they illustrate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the old 4 screenshots with 7 current ones covering chat,
General/Apple Intelligence settings, MCP + Personal Data settings,
Agent Skills (list + edit), Advanced settings, and iCloud Backup —
placed next to the sections they illustrate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Local-first policy: no telemetry/analytics/crash reporting, API keys
in Keychain, data flow documented for every opt-in feature that
touches personal data or talks to third-party AI providers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Local-first policy: no telemetry/analytics/crash reporting, API keys
in Keychain, data flow documented for every opt-in feature that
touches personal data or talks to third-party AI providers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>