Add export connectors to onboarding and desktop apps#6373
Conversation
…mport # Conflicts: # desktop/Desktop/Sources/GmailReaderService.swift
Greptile SummaryThis PR adds an Exports onboarding step (after DataSources) and export-destination cards on the Apps page, supporting Obsidian (automated file write), Notion (clipboard copy-and-open), and ChatGPT/Claude/Gemini (prompt + memory-pack copy-and-open).
Confidence Score: 4/5Not safe to merge — HANDOFF file explicitly flags UX as unacceptable and requests author confirmation before landing. Two P1 findings: the committed WIP handoff document signals the PR is intentionally incomplete, and the non-existent Notion API version string is a latent runtime failure if the dead code path is ever reactivated. HANDOFF-exports-after-import.md and desktop/Desktop/Sources/MemoryExportService.swift Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User clicks Export destination] --> B{destination}
B -->|Obsidian| C{vault path set?}
C -->|No| D[NSOpenPanel: pick vault]
D --> E[exportToObsidian: write Omi/Memories.md]
C -->|Yes| E
E --> F[Open obsidian://open URL]
B -->|Notion| G[prepareManualExport]
G --> H[Fetch memories up to 250]
H --> I[Build markdown pack]
I --> J[Copy markdown to clipboard]
J --> K[Reveal file in Finder]
K --> L[Open notion.so in browser/app]
B -->|ChatGPT / Claude / Gemini| M[prepareManualExport]
M --> N[Fetch memories up to 400]
N --> O[Build markdown with prompt prefix]
O --> P[Copy prompt + markdown to clipboard]
P --> Q[Reveal file in Finder]
Q --> R[Open target URL in browser/app]
Reviews (1): Last reviewed commit: "Polish export connectors and add brand l..." | Re-trigger Greptile |
| Branch: `codex/exports-after-import` | ||
| Worktree: `/Users/nik/projects/omi-exports-after-import` | ||
|
|
||
| Current state: | ||
| - Export onboarding step exists after imports. | ||
| - Apps page has export cards. | ||
| - Local test bundle: `com.omi.exports-after-import-local` | ||
| - The current UX is not acceptable yet: | ||
| - ChatGPT/Claude/Gemini manual export does not auto-paste into destination apps. | ||
| - Notion and Obsidian flows feel too heavy and should be simplified. | ||
|
|
||
| What likely needs work next: | ||
| 1. Make manual exports copy the generated memory pack/prompt automatically and open the destination reliably. | ||
| 2. Simplify Notion export UX: | ||
| - Prefer one-click connect or reuse existing Notion integration if possible. | ||
| - Avoid token + parent page friction if there is a simpler in-repo path. | ||
| 3. Simplify Obsidian export UX: | ||
| - Prefer folder/vault selection once, then one-click export. | ||
| 4. Test on Mac mini instead of local machine when possible. | ||
|
|
||
| Key files: | ||
| - `desktop/Desktop/Sources/MemoryExportService.swift` | ||
| - `desktop/Desktop/Sources/OnboardingExportsStepView.swift` | ||
| - `desktop/Desktop/Sources/MainWindow/Pages/MemoryExportDestinationSheet.swift` | ||
| - `desktop/Desktop/Sources/MainWindow/Pages/AppsPage.swift` | ||
| - `desktop/Desktop/Sources/OnboardingFlow.swift` | ||
| - `desktop/Desktop/Sources/OnboardingView.swift` | ||
|
|
||
| Notes: | ||
| - User wants this on a separate worktree and does not want anything merged until they confirm. | ||
| - Main branch had newer onboarding/import UI changes; this branch was merged on top of updated main but still needs product polish and verification. |
There was a problem hiding this comment.
WIP handoff artifact committed to repo
This file is an internal note that explicitly states "User wants this on a separate worktree and does not want anything merged until they confirm" and that the current UX is "not acceptable yet." Committing it to main signals the PR is intentionally in a draft state. Remove this file before landing.
| static let shared = MemoryExportService() | ||
|
|
||
| private let defaults = UserDefaults.standard | ||
| private let notionVersion = "2026-03-11" |
There was a problem hiding this comment.
Non-existent Notion API version
"2026-03-11" is a future-dated version string the Notion API does not recognise. The current stable version header is "2022-06-28". While exportToNotion is currently unreachable from the UI, it remains live code and would return HTTP 400 on every call if ever invoked.
| private let notionVersion = "2026-03-11" | |
| private let notionVersion = "2022-06-28" |
| func exportToNotion(token: String, parentPageID: String) async throws -> MemoryExportResult { | ||
| let sanitizedToken = token.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| let sanitizedParentPageID = parentPageID.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !sanitizedToken.isEmpty, !sanitizedParentPageID.isEmpty else { | ||
| throw MemoryExportError.invalidNotionConfiguration | ||
| } | ||
|
|
||
| let memories = try await fetchMemories(limit: 250) | ||
| guard !memories.isEmpty else { throw MemoryExportError.noMemories } | ||
|
|
||
| let pageTitle = "Omi Memory Export \(Self.exportTitleFormatter.string(from: Date()))" | ||
| let pageID = try await createNotionPage( | ||
| token: sanitizedToken, | ||
| parentPageID: sanitizedParentPageID, | ||
| title: pageTitle | ||
| ) | ||
| try await appendNotionBlocks( | ||
| token: sanitizedToken, | ||
| pageID: pageID, | ||
| memories: memories | ||
| ) | ||
|
|
||
| defaults.set(sanitizedToken, forKey: MemoryExportDestination.notion.notionTokenKey) | ||
| defaults.set(sanitizedParentPageID, forKey: MemoryExportDestination.notion.notionParentPageKey) | ||
|
|
||
| let detail = "Exported to Notion" | ||
| persistStatus( | ||
| destination: .notion, | ||
| exportedCount: memories.count, | ||
| detailText: detail, | ||
| filePath: nil | ||
| ) | ||
|
|
||
| return MemoryExportResult( | ||
| memoryCount: memories.count, | ||
| detailText: detail, | ||
| destinationURL: URL(string: "https://www.notion.so/"), | ||
| fileURL: nil, | ||
| clipboardText: nil | ||
| ) | ||
| } |
There was a problem hiding this comment.
exportToNotion is unreachable dead code
The full Notion API integration (exportToNotion, createNotionPage, appendNotionBlocks) is never called from any UI path — MemoryExportDestinationSheetModel.run() routes .notion to prepareManualExport (clipboard copy-open) instead. The notionToken and notionParentPageID published properties on the sheet model are similarly unused. This code should either be wired to a UI path or removed to avoid confusion.
| return | ||
| "Updated \(RelativeDateTimeFormatter().localizedString(for: lastExportedAt, relativeTo: Date()))" |
There was a problem hiding this comment.
RelativeDateTimeFormatter allocated on every render
A new formatter is constructed inside a computed property evaluated on every MemoryExportCard re-render. Formatters are expensive to create; promote it to a static constant.
| return | |
| "Updated \(RelativeDateTimeFormatter().localizedString(for: lastExportedAt, relativeTo: Date()))" | |
| private static let relativeDateFormatter = RelativeDateTimeFormatter() | |
| private var secondaryText: String? { | |
| if let lastExportedAt = status.lastExportedAt { | |
| return | |
| "Updated \(Self.relativeDateFormatter.localizedString(for: lastExportedAt, relativeTo: Date()))" |
| private func inlineTextField( | ||
| _ placeholder: String, | ||
| text: Binding<String>, | ||
| secure: Bool = false | ||
| ) -> some View { | ||
| Group { | ||
| if secure { | ||
| SecureField(placeholder, text: text) | ||
| } else { | ||
| TextField(placeholder, text: text) | ||
| } | ||
| } | ||
| .textFieldStyle(.plain) | ||
| .font(.system(size: 13)) | ||
| .foregroundColor(OmiColors.textPrimary) | ||
| .padding(.horizontal, 14) | ||
| .padding(.vertical, 12) | ||
| .background( | ||
| RoundedRectangle(cornerRadius: 16, style: .continuous) | ||
| .fill(OmiColors.backgroundSecondary) | ||
| .overlay( | ||
| RoundedRectangle(cornerRadius: 16, style: .continuous) | ||
| .stroke(Color.white.opacity(0.08), lineWidth: 1) | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
) ## Summary - add an Exports step after imports in desktop onboarding - add export connector cards to the desktop Apps page - support lower-friction export flows for Notion and Obsidian plus copy-and-open flows for ChatGPT, Claude, and Gemini - add real Obsidian and Gemini logos to the shared connector icon set ## Testing - cd desktop/Desktop && swift test --filter OnboardingFlowTests
Summary
Testing