Files
linkding-ios/openwiki/workflows/product-workflows.md
Krishna Kumar 8e1d94a4ea
All checks were successful
CI / build-and-deploy (push) Successful in 25s
Add source editing to ingest
2026-07-02 16:35:02 -05:00

12 KiB

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.

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.

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.