# Product Workflows This page describes the user-facing behavior implemented by the app and the source files that own each flow. ## Browse bookmarks Primary sources: `/Marks/Views/BookmarksView.swift`, `/Marks/Views/BookmarkListRow.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Services/LinkdingAPI.swift`. The Bookmarks tab loads linkding bookmarks through `BookmarksViewModel.load()`. The view supports loading skeletons, pull-to-refresh, pagination as the user scrolls, an unread filter, and row actions. A bookmark row displays title/domain/date context, unread state, available excerpt/AI summary, tags/AI tags, reading progress, and podcast-cache state. Tapping opens `BrowserView`. Swipe/context actions support archive, delete, edit, Safari open, and podcast conversion. Change guidance: - Keep row actions aligned across `BookmarksView`, `BookmarkListRow`, `BookmarkRow`, and `BrowserView`. - `BookmarksViewModel.saveToCache(_:)` intentionally skips saving when the unread filter is active, so filtered results do not replace the full cache. - Pagination uses linkding's `next` URL through `fetchBookmarksFromUrl(_:)`; do not rebuild next URLs unless the linkding API contract changes. ## Add and edit bookmarks in app Primary sources: `/Marks/Views/AddBookmarkView.swift`, `/Marks/Views/EditBookmarkView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Models/Bookmark.swift`. The Add view collects URL, optional title, description/notes, and comma-separated tags, normalizes/validates the URL, and calls `BookmarksViewModel.addBookmark(url:title:description:tags:)`. It also has a paste/import text path that uses `IngestPayloadParser` to extract the first `http`/`https` URL from plain text and preserve surrounding notes as the bookmark description. Added bookmarks are inserted locally, indexed into Spotlight, and enriched in a background task. The Edit view updates URL, title, description, tags, unread, and shared state through `BookmarkUpdate`. `BookmarksViewModel.updateBookmark(_:)` preserves locally stored AI summary/tags when replacing the bookmark returned from linkding. ## Search and semantic search Primary sources: `/Marks/Views/SearchView.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Services/ClaudeService.swift`. The Search tab has two paths: - normal search through linkding's bookmark API (`q` parameter), - semantic search through `ClaudeService.semanticSearch(query:in:)`, which fetches up to 200 bookmarks and sends a compact list to the backend for ranking. `SearchMarksIntent` routes spoken/system search text into `IntentRouter`, `MainContainer` switches to the Search tab, and `SearchView` consumes the router text into its visible search field. Change guidance: - Search state lives on the shared `BookmarksViewModel`; search and unread filters can affect list state outside Search. - Backend semantic search sends bookmark titles/domains and optional summaries to the Marks backend. Preserve that privacy boundary in UI copy and docs. ## Tags Primary sources: `/Marks/Views/TagsView.swift`, `/Marks/Models/Bookmark.swift`. The Tags tab derives tag counts from both linkding tags (`Bookmark.tagNames`) and local AI tags (`Bookmark.aiTags`), sorted by count/name. Tapping a tag shows matching bookmarks and reuses bookmark row/browser flows. If changing tag semantics, check in-app Add/Edit, share extension tag parsing, linkding `fetchTags()`, AI suggestions, Tags tab counts, and App Intents bookmark entity fields. ## AI enrichment and smart collections Primary sources: `/Marks/Services/ClaudeService.swift`, `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/CollectionsView.swift`, `/Marks/Services/AISummaryStore.swift`. AI enrichment generates a short summary and tags for a bookmark. The app stores AI metadata locally in `UserDefaults.standard` under `aiData`, keyed by bookmark ID, and mirrors summaries by URL through `AISummaryStore` for podcast/list display. Enrichment can happen in three ways: - after adding a bookmark, - as a silent background pass over up to five loaded bookmarks without summaries, - manually through Enrich All. Smart Collections send up to 150 bookmarks to the backend and render the returned collection names, descriptions, and bookmark IDs in `CollectionsView`. Caveat: README language mentions OpenRouter/Gemini directly, but current source uses the Marks backend proxy for these calls. ## Ask your bookmarks Primary sources: `/Marks/Views/AskView.swift`, `/Marks/Services/BookmarkAssistant.swift`, `/Marks/Services/BookmarkSearchTool.swift`, `/Marks/Services/SpotlightBookmarkSearch.swift`, `/Marks/Services/SpotlightIndexer.swift`. Ask is an on-device RAG workflow. `BookmarkAssistant` creates a `FoundationModels` language model session with a bookmark search tool. The model instructions say bookmarks are the only source of truth and require the tool for lookup. The retrieval corpus comes from Spotlight. `BookmarksViewModel` indexes bookmarks after loading, loading more, adding, and enrichment. Deleted/archived bookmarks are removed from Spotlight. Change guidance: - If Ask gives stale answers, inspect Spotlight indexing and removal paths first. - If changing indexed fields, update `BookmarkEntity`, `SpotlightIndexer`, search tooling, and tests where relevant. - The Ask path is separate from backend semantic search and does not use `ClaudeService`. ## Share extension save flow Primary sources: `/Marks/Models/IngestPayload.swift`, `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, `/Marks/Services/PodcastRequests.swift`, `/project.yml`. The share extension scans URL, HTML, RTF, plain-text, and text attachments, then `IngestPayloadParser` extracts the first bookmarkable web URL. For non-URL text payloads it also preserves cleaned surrounding text as notes. On load it: 1. reads linkding config from app-group defaults, 2. calls `LinkdingAPI.checkBookmark(url:)`, 3. pre-fills existing bookmark data or scraped metadata/linkding auto-tags, 4. fetches existing tag vocabulary and backend AI tag suggestions, 5. lets the user edit tags, notes, read-later state, and Create Podcast. On save, existing bookmarks are patched; new bookmarks are created. If Create Podcast is enabled, or if configured auto-tags match, the extension enqueues a `PodcastRequests` item instead of generating audio itself. Change guidance: - The extension is short-lived. Keep long-running work in the main app. - The extension depends on shared source files listed in `/project.yml`; adding service dependencies may require target membership updates. - `IngestPayloadParser` intentionally ignores non-HTTP(S) URLs and caps preserved notes at 1,800 characters. - `ShareExtension/Info.plist` currently uses a broad activation rule. Narrowing supported payloads is a product decision. ## Sources Primary sources: `/Marks/Views/SourcesView.swift`, `/Marks/Models/IngestedSource.swift`, `/Marks/Services/IngestedSourceStore.swift`, `/Marks/Services/SourceSpotlightIndexer.swift`. The Sources tab stores non-web material in Marks. Users can: - import typed/pasted text with an optional title and tags, - import PDFs, plain text files, generic text/data files through the document picker, - edit source title, tags, and stored body/extracted text, - search across source title, body text, tags, and original filename, - open imported originals through QuickLook, - delete sources and copied files, - create or play a podcast from extractable source text. `IngestedSourceLibrary.load()` reads `Sources/sources.json` from Application Support and indexes sources into Spotlight. Text imports write metadata/body text directly. File imports copy the selected security-scoped file into `Sources/files/`; PDFs extract text with PDFKit, while other files attempt UTF-8/ASCII extraction. Editing an imported file source updates Marks' stored extracted text and metadata, but does not modify the copied original file. Change guidance: - Empty or image-only PDFs may import with no extractable text; podcast actions are disabled when `podcastText` is empty. - Source podcasts use `marks-source://{uuid}` as the cache/index URL and send extracted text plus `source_kind` to the backend. - If changing source storage, update editing, deletion, Spotlight removal, QuickLook file lookup, and source podcast cache behavior together. ## Podcasts Primary sources: `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Services/PodcastLibrary.swift`, `/Marks/Services/ClaudeService.swift`, `/Marks/Services/PodcastRequests.swift`, `/Marks/Info.plist`. Podcast generation converts an article URL, or a locally ingested source with extracted text, into audio through the Marks backend: 1. create generation job (`/v1/podcast/generate`), 2. poll status every few seconds, 3. download audio when done, 4. save MP3 under Application Support `podcasts/`, 5. upsert `PodcastIndex`, 6. reload `PodcastLibrary`, 7. write recent podcast metadata for widgets. `BookmarksViewModel.playOrGeneratePodcast(...)` prevents interrupting current bookmark playback. If the player is idle, it starts foreground generation/playback and callers present the player. If busy, it enqueues background generation through `PodcastGenerationManager`. `SourcesView` follows the same foreground/background pattern for local sources, using `marks-source://{uuid}` plus `sourceText`/`sourceKind`. The player uses AVFoundation/MediaPlayer for playback, background audio, progress, saved playback speed, restored paused session, queue playback, now-playing metadata, remote controls, sleep timer, and played/unplayed state. Change guidance: - The main app plist enables background audio and the custom `marks` URL scheme. - Widget podcast metadata is in the app group, but audio files and `PodcastIndex` are in the main app's Application Support directory. - Share-extension queued podcast requests are drained by `BookmarksViewModel.processPendingPodcastRequests()` on launch/foreground. Consider retry semantics before changing drain behavior. - Source-backed podcast generation depends on backend support for the optional `text` and `source_kind` fields in `POST /v1/podcast/generate`. ## Widgets and deep links Primary sources: `/MarksWidget/WidgetDataStore.swift`, `/MarksWidget/RecentBookmarksWidget.swift`, `/MarksWidget/RandomBookmarkWidget.swift`, `/MarksWidget/RecentPodcastsWidget.swift`, `/Marks/MarksApp.swift`. Widgets are offline readers of app-group JSON data: - Recent Bookmarks shows up to three recent bookmarks and links rows to `marks://bookmark`. - Random Bookmark chooses a random cached bookmark and links the whole widget to `marks://bookmark`. - Recent Podcasts shows up to three podcasts and links rows to `marks://podcast`. The main app handles bookmark links by presenting `BrowserView`; podcast links start playback and show `PodcastPlayerView`. ## App Intents, Siri, and Shortcuts Primary sources: `/Marks/Intents/MarksAppIntents.swift`, `/Marks/Intents/BookmarkEntity.swift`, `/Marks/Intents/IntentSupport.swift`, `/Marks/Intents/BookmarkOnscreen.swift`, `/MarksTests/AppIntentsTests.swift`. Implemented intents: - Open Bookmark — routes a selected entity URL into the app browser. - Search Marks — system search schema; opens Search tab with criteria text. - Add Bookmark — headless linkding save and widget refresh. - Show Unread Bookmarks — fetches up to ten unread items. - Summarize Bookmark — backend AI summary for a selected bookmark. `BookmarkEntity` maps linkding bookmarks into App Intents entities. Browser presentations annotate current bookmarks for on-screen entity references. Tests currently cover entity mapping, router behavior, direct no-network open/search intent perform calls, and shortcut registration.