add openwiki docs
All checks were successful
CI / build-and-deploy (push) Successful in 17s

This commit is contained in:
Krishna Kumar
2026-07-02 00:33:49 -05:00
parent b496e607fc
commit 4d875e12d6
6 changed files with 566 additions and 0 deletions

10
AGENTS.md Normal file
View File

@@ -0,0 +1,10 @@
## OpenWiki
This repository has documentation located in the /openwiki directory.
Start here:
- [OpenWiki quickstart](openwiki/quickstart.md)
OpenWiki includes repository overview, architecture notes, workflows, domain concepts, operations, integrations, testing guidance, and source maps.
When working in this repository, read the OpenWiki quickstart first, then follow its links to the relevant architecture, workflow, domain, operation, and testing notes.

View File

@@ -0,0 +1,11 @@
{
"updatedAt": "2026-07-02T05:28:44Z",
"gitHead": "b496e607fc37e795c765355dd99c191518e6f7f2",
"mode": "init",
"pages": [
"openwiki/quickstart.md",
"openwiki/architecture/app-architecture.md",
"openwiki/workflows/product-workflows.md",
"openwiki/operations/development-and-testing.md"
]
}

View File

@@ -0,0 +1,160 @@
# App Architecture
This page explains how the major Marks runtime pieces fit together. For product-level flows, see [Product workflows](../workflows/product-workflows.md). For setup and verification, see [Development and testing](../operations/development-and-testing.md).
## Targets and source layout
`/project.yml` is the XcodeGen project definition. It defines four targets:
- `Marks` — the main iOS app. Sources include `/Marks` plus `/MarksWidget/WidgetDataStore.swift` so the app can write widget data.
- `ShareExtension` — app extension with `/ShareExtension` plus selected shared model/service files from `/Marks` (`Bookmark`, `ServerConfig`, `LinkdingAPI`, `MarksAuth`, `Log`, `PodcastRequests`).
- `MarksWidget` — WidgetKit extension with `/MarksWidget`.
- `MarksTests` — unit-test bundle depending on `Marks`.
The app is Swift 6.0 and targets iOS 26.0. The main app, share extension, and widget extension share the app group `group.com.magicive.marks` through entitlements and runtime code.
## App entrypoint and navigation shell
`/Marks/MarksApp.swift` contains the `@main` app and `MainContainer`.
At startup:
1. `MarksApp` loads a saved `ServerConfig` or falls back to a built-in default config.
2. `MainContainer` constructs `LinkdingAPI(config:)`.
3. `MainContainer` stores `BookmarksViewModel(api:cacheKey:)` in `@State`, keeping the model stable across SwiftUI re-renders.
4. A top-level `TabView` presents Bookmarks, Tags, Podcasts, and Search.
`MainContainer` also handles:
- custom deep links (`marks://bookmark?...`, `marks://podcast?...`),
- App Intent handoff through `IntentRouter.shared`,
- pending podcast requests queued by the share extension,
- foreground refreshes and background podcast progress persistence.
## Central state: `BookmarksViewModel`
`/Marks/Views/BookmarksViewModel.swift` is `@Observable @MainActor` and is the core app state object. It owns:
- loaded bookmarks, errors, loading flags, pagination URL, search query, and unread filter,
- generated smart collections and enrichment progress,
- a `LinkdingAPI` actor for server sync,
- `ClaudeService` for backend AI/podcast calls,
- `PodcastPlayerViewModel` and `PodcastGenerationManager`,
- the disk bookmark cache URL.
Key side effects during bookmark loading and mutations:
- `load()` optionally restores cached bookmarks from Application Support, fetches linkding bookmarks, restores local AI metadata, saves the full unfiltered cache, writes recent bookmarks to `WidgetDataStore`, and indexes bookmarks into Spotlight.
- `loadMore()` follows linkding's `next` URL and indexes newly appended bookmarks.
- `delete()` and `archive()` mutate linkding, remove local entries, and remove Spotlight entries.
- `addBookmark()` saves to linkding, inserts locally, indexes the bookmark, and starts best-effort AI enrichment.
- `enrichAll()` and background `startEnrichment()` call AI enrichment, persist AI metadata, and reindex enriched bookmarks.
Because one `BookmarksViewModel` instance is shared by all tabs, array mutations and filters have cross-tab effects.
## Linkding integration
`/Marks/Services/LinkdingAPI.swift` is an actor wrapping linkding's REST API. It builds URLs from `ServerConfig` (`/Marks/Models/ServerConfig.swift`) and sends `Authorization: Token ...` headers.
Implemented operations include:
- `fetchBookmarks(search:offset:limit:unread:)``GET /api/bookmarks/`,
- `fetchBookmarksFromUrl(_:)` for pagination `next` URLs,
- `checkBookmark(url:)``GET /api/bookmarks/check/`, used by the share extension,
- `fetchTags(limit:)``GET /api/tags/`,
- `createBookmark(_:)``POST /api/bookmarks/`,
- `updateBookmark(id:update:)``PATCH /api/bookmarks/{id}/`,
- `deleteBookmark(id:)``DELETE /api/bookmarks/{id}/`,
- `archiveBookmark(id:)``POST /api/bookmarks/{id}/archive/`,
- `verifyConnection()` for configuration checks.
`Bookmark`, `BookmarkResponse`, create/update payloads, `BookmarkCheck`, tag response models, and `SmartCollection` live in `/Marks/Models/Bookmark.swift`.
## Configuration and app-group sharing
`ServerConfig` contains linkding host, optional port, path, token, and HTTPS flag. Its extension stores config in two places:
- `UserDefaults.standard` for the main app,
- `UserDefaults(suiteName: "group.com.magicive.marks")` for extensions.
`ServerConfig.loadShared()` is used by the share extension, which cannot read the main app's standard defaults. Avoid changing config keys or app-group IDs without updating all entitlements, `ServerConfig`, `WidgetDataStore`, and `PodcastRequests` together.
Security note: `/Marks/MarksApp.swift` currently includes a built-in default config with a token-like value. Treat it as sensitive; do not copy it into documentation or generated output.
## Backend AI, analytics, and podcasts
`/Marks/Services/MarksAuth.swift` registers an anonymous device with the Marks backend and caches a JWT in `UserDefaults.standard`. `ClaudeService` uses that token for backend calls:
- bookmark enrichment: `POST /v1/marks/enrich`,
- semantic search: `POST /v1/marks/search`,
- smart collections: `POST /v1/marks/collections`,
- podcast generation: `POST /v1/podcast/generate`,
- podcast polling: `GET /v1/podcast/status/{jobId}`,
- podcast audio download: `GET /v1/podcast/audio/{jobId}`.
`/Marks/Services/AnalyticsService.swift` sends events through the backend path used by `ClaudeService`/`MarksAuth`.
Podcast audio is downloaded to Application Support under a `podcasts` directory using a stable hash of the article URL. `PodcastIndex` records metadata in `index.json`; `PodcastLibrary` reads that index for the Podcasts tab and widget metadata.
## On-device Ask and Spotlight
The app has two AI paths:
1. **Backend AI** through `ClaudeService` for enrichment, semantic search, smart collections, and podcasts.
2. **On-device RAG** through `/Marks/Services/BookmarkAssistant.swift` for the Ask UI.
`BookmarkAssistant` uses `FoundationModels.LanguageModelSession` with `BookmarkSearchTool`. Its instructions require answers to be grounded in bookmarks. Retrieval flows through Spotlight:
- `SpotlightIndexer` indexes bookmarks after loads, load-more operations, add, and enrichment.
- `SpotlightBookmarkSearch`/`BookmarkSearchTool` search the index for relevant bookmarks.
- `BookmarkEntity` also conforms to indexed App Intents concepts for system-level entity discovery.
If changing bookmark fields, keep `Bookmark`, `BookmarkEntity`, Spotlight indexing, widget payloads, and row UI in sync.
## Share extension architecture
The share extension is implemented by `/ShareExtension/ShareViewController.swift` and `/ShareExtension/ShareView.swift`.
It accepts a shared URL/text payload, creates a SwiftUI save card, loads shared `ServerConfig`, checks linkding for an existing bookmark, and lets the user edit tags, notes, read-later state, and a Create Podcast toggle.
For suggestions, it combines:
- linkding `checkBookmark` metadata and `auto_tags`,
- the user's existing tag vocabulary from `fetchTags()`,
- backend AI tag suggestions through `/ShareExtension/TagSuggester.swift`.
Existing bookmarks are updated rather than duplicated. New bookmarks are created through `LinkdingAPI`. Podcast generation is not performed inside the extension; it queues a `PodcastRequests` item in the app group for the main app to process later.
## Widget architecture and deep links
`/MarksWidget/MarksWidgetBundle.swift` registers three widgets:
- `RecentBookmarksWidget` (`systemMedium`),
- `RandomBookmarkWidget` (`systemSmall`),
- `RecentPodcastsWidget` (`systemMedium`).
Widgets read only app-group JSON through `/MarksWidget/WidgetDataStore.swift`:
- `widget_bookmarks.json`,
- `widget_podcasts.json`.
They do not call network APIs. Main-app writes trigger `WidgetCenter.shared.reloadAllTimelines()` when WidgetKit is available.
Widget links use custom URLs built by `URL.marksBookmark(_:)` and `URL.marksPodcast(_:)`. `MainContainer.handleDeepLink(_:)` opens bookmark URLs in `BrowserView` or starts podcast playback and shows the player.
## App Intents and Shortcuts
`/Marks/Intents/MarksAppIntents.swift` defines:
- `OpenBookmarkIntent` — opens a selected `BookmarkEntity` in app.
- `SearchMarksIntent` — conforms to the system search schema and routes text to the Search tab.
- `AddBookmarkIntent` — saves a URL headlessly through linkding and refreshes widget data.
- `ShowUnreadIntent` — fetches up to 10 unread bookmarks.
- `SummarizeBookmarkIntent` — calls backend AI enrichment for a selected bookmark.
- `MarksShortcuts` — registers Add Bookmark, Search, Unread, and Summarize shortcuts.
`IntentRouter` bridges intents that need to open UI back into `MainContainer`. `BookmarkEntity` maps between linkding `Bookmark` values and App Intents entities.
## Recent history context
Recent git history shows major investment in podcast UX and on-device AI: the latest merge added played/unplayed queues, background podcast generation, share-sheet audio requests, podcast library/index changes, and player improvements. A prior AI commit added on-device RAG, Spotlight indexing, and unified logging. Use those areas cautiously because they span app state, storage, widgets, share extension, and App Intents rather than a single view.

View File

@@ -0,0 +1,155 @@
# Development and Testing
This page covers local setup, generated project guidance, runtime configuration/storage, and verification strategy for changes.
## Local setup
Requirements from `/README.md` and `/project.yml`:
- macOS with Xcode capable of building the declared iOS 26 / Swift 6 project.
- [XcodeGen](https://github.com/yonaskolb/XcodeGen).
- A linkding server with API access for real sync testing.
- Access to the Marks backend for AI, semantic search, smart collections, analytics, and podcast generation flows.
Setup:
```bash
brew install xcodegen
xcodegen generate
open Marks.xcodeproj
```
Build and run the `Marks` scheme in Xcode. `/project.yml` declares schemes for `Marks`, `MarksWidget`, and `ShareExtension`; the `Marks` scheme builds the app, share extension, and widget extension and runs `MarksTests` in the test action.
## Project generation rules
Use `/project.yml` as the source of truth for targets, source membership, entitlements, and dependencies. Regenerate the Xcode project after changing target membership:
```bash
xcodegen generate
```
Common target-membership traps:
- The main app includes `/MarksWidget/WidgetDataStore.swift` directly so it can write widget JSON.
- The share extension includes selected shared files from `/Marks/Models` and `/Marks/Services`; if the extension needs new model/service code, add it in `/project.yml`.
- The app, widget, and share extension all depend on matching app-group entitlements.
## Testing
Current automated test coverage is concentrated in `/MarksTests/AppIntentsTests.swift`.
Covered today:
- `BookmarkEntity` mapping from `Bookmark`, fallback title behavior, and round-trip fields.
- `EntityIdentifier` encoding.
- `IntentRouter` open/search setters.
- Direct no-network `perform()` tests for Search and Open intents.
- App Shortcuts registration count.
Not currently covered by repository tests:
- `BookmarksViewModel` load/cache/pagination/mutation flows.
- Linkding networking with mocks.
- AI enrichment, semantic search, smart collections, or podcast backend calls.
- Share extension save/update/tag suggestion flows.
- Widget timeline rendering/deep links.
- Browser reading progress/reader mode.
- Podcast generation, playback, queue, played/unplayed, and background audio behavior.
Suggested test command:
```bash
xcodebuild test -scheme Marks -destination 'platform=iOS Simulator,name=iPhone 16'
```
Use an installed simulator name if `iPhone 16` is unavailable.
## Manual verification checklist by area
Bookmark sync/list changes:
- Launch with valid linkding config.
- Confirm cached bookmarks render quickly, then network refresh updates the list.
- Test pull-to-refresh, pagination, unread filter, add, edit, archive, delete.
- Confirm widget bookmark JSON updates and Spotlight indexing still happens.
Search/tag changes:
- Test linkding search and semantic search separately.
- Verify unread filter interactions.
- Confirm Tags tab counts include expected linkding and AI tags.
AI changes:
- Test enrichment on add, background enrichment, Enrich All, semantic search, Smart Collections, and Summarize intent if affected.
- Test Ask on a device/simulator environment where Apple on-device model availability can be observed; handle unavailable states.
- Preserve backend privacy expectations: URLs/titles/tags/summaries may be sent to the Marks backend for backend AI paths.
Share extension changes:
- Share a new URL and an already-saved URL.
- Verify duplicate-aware update behavior.
- Test linkding auto-tags, AI suggestion chips, tag parsing, notes, read-later, and Create Podcast.
- Confirm the extension works when shared config is missing; current UI shows an "Open Marks first" style failure.
Podcast changes:
- Generate from bookmark row and Browser view.
- Start generation while another podcast is playing; confirm background generation does not interrupt playback.
- Test queue playback, played/unplayed state, delete, sleep timer, saved speed, background audio, remote controls, now-playing metadata, relaunch restore, and widget recent podcasts.
- Share a URL with Create Podcast and confirm the main app drains and starts the queued request.
Widget/deep-link changes:
- Confirm `widget_bookmarks.json` and `widget_podcasts.json` are refreshed by main-app writes.
- Test Recent Bookmarks, Random Bookmark, and Recent Podcasts widgets.
- Tap bookmark and podcast widget entries and verify `marks://bookmark` and `marks://podcast` routing in `MainContainer`.
App Intents changes:
- Run `MarksTests`.
- Test Shortcuts/Siri flows for Add Bookmark, Search, Show Unread, Summarize, and Open Bookmark.
- Verify entity queries still fetch/resolve bookmarks correctly with a configured linkding server.
## Configuration and persistence map
Do not read or document live secret values.
Main app / standard defaults:
- `serverConfig` — linkding config saved by `ServerConfig.save()`.
- `marksDeviceId`, `marksJWT`, `marksJWTExpiry` — Marks backend auth state from `MarksAuth`.
- `aiData` — AI summaries/tags by bookmark ID.
- `aiSummaryByURL` — summary mirror used outside the main bookmark array.
- `podcastProgress`, `podcastPlaybackSpeed`, `podcastSession` — podcast playback state.
- `readingProgress` — in-browser reading progress.
App-group defaults/files (`group.com.magicive.marks`):
- `serverConfig` — shared linkding config for extensions.
- `pendingPodcastRequests` — share extension to main app podcast queue.
- `podcastAutoTags` — tags that auto-enable podcast creation in the share extension.
- `widget_bookmarks.json` — recent bookmark widget cache.
- `widget_podcasts.json` — recent podcast widget cache.
Application Support files:
- `bookmarks_cache.json` or `bookmarks_{host}.json` — cached bookmarks.
- `podcasts/{hash}.mp3` — generated podcast audio.
- `podcasts/index.json` — podcast metadata and played state.
## Known caveats and stale-doc risks
- `/Marks/MarksApp.swift` contains a built-in default linkding configuration with a token-like value. Treat it as sensitive and consider replacing it with a safer onboarding/configuration flow before distribution.
- `OnboardingView` exists but is not the active first-launch path in `MarksApp` as currently written.
- Disconnect behavior currently resets via the default config callback path instead of clearly deleting config and returning to onboarding.
- README AI provider text appears older than source: current backend AI/podcast features go through `MarksAuth` + `ClaudeService`, while Ask uses Apple's on-device `FoundationModels`.
- `MarksAuth` stores backend JWT/device state in `UserDefaults`, not Keychain.
- App-group ID strings are duplicated in code and entitlements; update all occurrences together if changing it.
- Share-extension podcast queue draining clears requests before generation success, so crash/retry semantics deserve care.
- Recent git history shows podcast and on-device AI features were added across many files; avoid narrow changes that ignore cross-target data flow.
## Existing design guidance
`/docs/design-wwdc26.md` is a useful source for UI direction. It emphasizes native iOS 26 UI/Liquid Glass behavior, editorial bookmark rows as the brand canvas, Dynamic Type-safe row layout, bottom/reachable search patterns, WidgetKit app-group data, and standard platform affordances. Link to it rather than duplicating the full design brief.

81
openwiki/quickstart.md Normal file
View File

@@ -0,0 +1,81 @@
# Marks OpenWiki Quickstart
Marks is a native SwiftUI iOS client for [linkding](https://github.com/sissbruecker/linkding), a self-hosted bookmark manager. The app targets iOS 26 and Swift 6, and includes a main app, share extension, widget extension, App Intents, AI-assisted bookmark features, and podcast generation/playback.
Start here when changing the repository, then follow the page links below for the area you are touching.
## What this repository contains
- **Main iOS app** in `/Marks` with SwiftUI tabs for bookmarks, tags, podcasts, and search. The entrypoint is `/Marks/MarksApp.swift`.
- **Models** in `/Marks/Models`, centered on `Bookmark`, linkding response/create/update payloads, tags, smart collections, and `ServerConfig`.
- **Services** in `/Marks/Services` for linkding networking, AI/backend calls, auth, on-device bookmark Q&A, Spotlight indexing, widget/podcast data, analytics, and logging.
- **Views** in `/Marks/Views` for the primary product surfaces. `BookmarksViewModel` is the central app state object.
- **App Intents** in `/Marks/Intents` for opening, searching, adding, listing unread, and summarizing bookmarks.
- **Share extension** in `/ShareExtension` for saving shared URLs/text to linkding and queueing podcast generation.
- **Widget extension** in `/MarksWidget` for recent bookmarks, random bookmark, and recent podcast widgets.
- **Tests** in `/MarksTests`, currently focused on the App Intents layer.
- **XcodeGen project spec** in `/project.yml`; `Marks.xcodeproj` is generated/checked in but `project.yml` is the project definition to inspect first.
- **Design notes** in `/docs/design-wwdc26.md` for iOS 26/Liquid Glass, search, widgets, and row-design guidance.
## Major documentation pages
- [Architecture notes](architecture/app-architecture.md) — target layout, state flow, networking, persistence, AI, Spotlight, extensions, widgets, and App Intents.
- [Product workflows](workflows/product-workflows.md) — how bookmark browsing, saving, AI enrichment, Ask, smart collections, podcasts, widgets, and intents behave.
- [Development and testing](operations/development-and-testing.md) — setup/build/test commands, generated project guidance, storage/config map, verification checklist, and caveats.
## Build and run
The README documents the intended local setup:
```bash
brew install xcodegen
xcodegen generate
open Marks.xcodeproj
```
Then build and run from Xcode using the `Marks` scheme. `/project.yml` defines Swift 6.0, iOS 26.0 deployment target, the `Marks`, `ShareExtension`, `MarksWidget`, and `MarksTests` targets, and the app-group entitlements used by all app surfaces.
Useful checks before shipping a change:
```bash
xcodegen generate
xcodebuild test -scheme Marks -destination 'platform=iOS Simulator,name=iPhone 16'
```
Adjust the simulator name to what is installed locally.
## Runtime architecture in one minute
At launch, `MarksApp` loads a `ServerConfig`, constructs `LinkdingAPI`, and stores a shared `BookmarksViewModel` in `MainContainer` so it survives SwiftUI re-renders (`/Marks/MarksApp.swift`, `/Marks/Views/BookmarksViewModel.swift`). The top-level `TabView` has Bookmarks, Tags, Podcasts, and Search tabs.
`BookmarksViewModel` fetches paginated bookmarks from linkding through `LinkdingAPI`, restores local AI metadata, caches bookmarks to Application Support, writes recent bookmark metadata for widgets, and indexes bookmarks into Spotlight. It also owns `ClaudeService`, `PodcastPlayerViewModel`, and `PodcastGenerationManager`, so AI, podcast, search, and list state are shared across tabs.
Extensions and widgets use the shared app group `group.com.magicive.marks`. `ServerConfig.save()` writes to standard defaults and the app-group defaults; `WidgetDataStore` writes app-group JSON files for widgets; `PodcastRequests` uses app-group defaults to hand podcast requests from the share extension to the main app.
## External systems and privacy boundaries
- **linkding** is the source of truth for bookmarks and tags. `LinkdingAPI` uses token auth and calls linkding bookmark, check, tag, create, update, delete, archive, and connection-test endpoints.
- **Marks backend** is used by `MarksAuth`/`ClaudeService` for AI enrichment, semantic search, smart collections, analytics events, and podcast generation/audio download.
- **Apple on-device intelligence and Spotlight** power the in-app Ask surface via `FoundationModels`, `CoreSpotlight`, and the `BookmarkSearchTool` retrieval path.
- **WidgetKit, App Intents, AVFoundation, and MediaPlayer** support widgets, Siri/Shortcuts integration, podcast playback, background audio, now-playing metadata, and remote controls.
Do not document or expose secret values. `/Marks/MarksApp.swift` currently contains a built-in default server configuration with a token-like value; treat it as sensitive source material and avoid copying it into docs, logs, screenshots, or tests.
## Important caveats for future agents
- The README says optional AI features use OpenRouter/Gemini directly. Current source routes backend AI/podcast calls through `MarksAuth` + `ClaudeService`; the in-app Ask feature is on-device via `FoundationModels`. Prefer source evidence when changing AI behavior.
- `OnboardingView` exists, but current launch flow uses saved-or-default config directly. Do not assume onboarding is active without changing `/Marks/MarksApp.swift`.
- Settings' disconnect callback currently resets to the built-in default config path rather than forcing onboarding. Confirm intended product behavior before changing account/config flows.
- `BookmarksViewModel` is shared across tabs, so changes to bookmark arrays, filters, search, enrichment, and pagination can affect Bookmarks, Tags, Search, Podcasts, widgets, and Spotlight indexing.
- Share-extension podcast requests are queued in shared defaults and drained by the main app on launch/foreground. Be careful with crash/retry semantics when touching that flow.
- Existing automated tests cover App Intents only. For UI, networking, sync, share extension, widgets, and podcasts, add tests where feasible and perform manual verification.
## Where to start for common changes
- Bookmark list, pagination, search, tags, enrichment state: `/Marks/Views/BookmarksViewModel.swift`, `/Marks/Views/BookmarksView.swift`, `/Marks/Services/LinkdingAPI.swift`.
- Linkding API changes: `/Marks/Services/LinkdingAPI.swift`, `/Marks/Models/Bookmark.swift`, `/ShareExtension/ShareView.swift`, `/Marks/Intents/IntentSupport.swift`.
- AI/backend changes: `/Marks/Services/ClaudeService.swift`, `/Marks/Services/MarksAuth.swift`, `/Marks/Services/BookmarkAssistant.swift`, `/Marks/Services/BookmarkSearchTool.swift`.
- Podcast generation/playback: `/Marks/Services/PodcastGenerationManager.swift`, `/Marks/Services/PodcastIndex.swift`, `/Marks/Views/PodcastPlayerView.swift`, `/Marks/Views/BookmarksViewModel.swift`.
- Share extension: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, plus shared files declared in `/project.yml`.
- Widgets/deep links: `/MarksWidget/WidgetDataStore.swift`, `/MarksWidget/*Widget.swift`, `/Marks/MarksApp.swift`.
- App Intents/Siri/Shortcuts: `/Marks/Intents/*`, `/MarksTests/AppIntentsTests.swift`.

View File

@@ -0,0 +1,149 @@
# 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, and comma-separated tags, normalizes/validates the URL, and calls `BookmarksViewModel.addBookmark(url:title:tags:)`. 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: `/ShareExtension/ShareViewController.swift`, `/ShareExtension/ShareView.swift`, `/ShareExtension/TagSuggester.swift`, `/Marks/Services/PodcastRequests.swift`, `/project.yml`.
The share extension extracts a URL/text payload and shows an interactive save card. 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.
- `ShareExtension/Info.plist` currently uses a broad activation rule. Narrowing supported payloads is a product decision.
## 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 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 playback. If the player is idle, it starts foreground generation/playback and callers present the player. If busy, it enqueues background generation through `PodcastGenerationManager`.
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.
## 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.