Commit Graph
19 Commits
Author SHA1 Message Date
Krishna KumarandClaude Opus 5 69556afc08 Fix Spotlight indexing: route the entity URL to contentURL
CI / build-and-deploy (push) Successful in 26s
CI / build-and-deploy (pull_request) Successful in 25s
Every bookmark failed to reach the Spotlight index. Each item died in
translation with "Provided object for field url is of class NSURL,
expected class: NSString", so on-device search and Ask Your Bookmarks
were retrieving from an index that was effectively empty.

Left to itself, App Intents indexes BookmarkEntity's `url` property under
the attribute set's own `url` key, which Spotlight's Cascade translator
types as NSString. Giving the property an explicit
`indexingKey: \.contentURL` sends it to a URL-typed field instead — and
contentURL is the right field for "where this content lives" regardless.
Keeping the property a URL rather than retyping it to String means
existing Shortcuts that read it are unaffected.

The attribute set now sets contentURL too, and the retrieval side reads
it back, so the round trip stays on one field.

Why it went unnoticed: translation happens after `indexAppEntities`
returns, so indexing logged success the whole time. Measured on the
simulator against the live library — 202 translation failures per launch
before, 0 after.

Adds SpotlightRetrievalTests, which indexes a bookmark and retrieves it
through the assistant's own path. Nothing weaker would have caught this,
since the failure was silent at every layer above the index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
2026-07-27 19:12:25 -05:00
Krishna KumarandClaude Opus 5 fffb3999bf Carry the library design across the rest of the app
CI / build-and-deploy (pull_request) Successful in 34s
CI / build-and-deploy (push) Successful in 34s
Every screen now speaks the paper vocabulary rather than only the
bookmarks screen: Search, Sources, Podcasts and the player, Settings,
Add/Edit, Smart Collections, Ask, Onboarding, the browser toolbar, the
Siri snippets, and the share extension's save card.

The change is mostly a design system rather than per-screen tweaks.
LibraryKit gains:

- Semantic colors — secondary/tertiary/faint text, `raised` surfaces,
  and accent/alarm/affirm, so screens stop reaching for .secondary,
  systemGray, .blue, .red and .green. The three status colors come out
  of the palette (the swatch blue, vermilion and forest) rather than
  from the system set, so the design keeps spending one set of inks.
- PaperType — the two voices made explicit. Prose (titles, summaries,
  anything a person wrote) is serif; anything the machine contributes
  (domains, dates, counts, tags, labels, buttons) is monospaced. Keeping
  that split strict is what makes the design read as archival rather
  than as decoration.
- paperSurface() / paperField() / paperCard() for grounds, inputs and
  raised blocks.
- PaperEmptyState, because ContentUnavailableView can't be restyled — it
  draws its own bold system type and grey, which was the loudest
  remaining system voice once the screens moved onto the sheet. All nine
  usages are converted.

The app also sets one accent for the system chrome it doesn't draw —
tab bar, search fields, switches, swipe actions — which otherwise stayed
system blue around paper screens.

BookmarkRow is deleted. Once Search moved to the library row and
TagBookmarksView was gone, nothing passed .classic, so the style fork in
BookmarkListRow went with it. There is one bookmark row now.

The share extension compiles LibraryKit directly, since its save card is
a user-facing surface and should not be the one place still using system
styling.

Verified on device in both color schemes; screens toured with a
temporary UI test harness (removed). Suite passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
2026-07-27 12:50:45 -05:00
Krishna KumarandClaude Opus 5 3c7a783b6c Fix tap-to-open on cards, and move the list to the library design
CI / build-and-deploy (pull_request) Successful in 20s
CI / build-and-deploy (push) Successful in 21s
Two changes.

Tapping a card did nothing. LibraryCard carried its own press-scale
effect via onLongPressGesture(minimumDuration: 0), and a zero-duration
long press fires immediately and swallows the tap the host attaches. It
was invisible in the prototype because nothing was listening for taps
there. LibraryCard is now purely presentational and the grid wraps it in
a Button with the existing RowPressStyle, which gets the same scale
without competing for the gesture.

The list now speaks the same design as the cards: paper ground, the
color chip where the favicon was, serif quoted title, monospaced
domain/date, serif-italic AI summary, monospaced tag chips. Everything
the old row carried is still there — unread state, excerpt, tags,
podcast affordance, reading progress — along with all swipe actions,
since it is still a List.

BookmarkListRow takes a style rather than being rewritten, because the
Tags and Search screens use the same row and should not be silently
restyled by a change aimed at the bookmarks screen.

The tag filter strip moved up to BookmarksView. Both layouts speak the
same language now, so the strip belongs to the screen rather than to one
mode — which also retires the rule that leaving cards mode had to clear
the tag filter to stop it becoming invisible.

The tag chip strip fades at its trailing edge; without it a tag clipped
mid-word reads as broken text rather than as something scrollable.

Verified in both layouts and both color schemes against the live server.
Full suite passes (16 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
2026-07-26 01:49:15 -05:00
Krishna KumarandClaude Opus 5 99add346fd Wire the library presentation into BookmarksView
CI / build-and-deploy (push) Successful in 19s
CI / build-and-deploy (pull_request) Successful in 20s
Adds a cards/list toggle to the bookmarks screen. List mode is the
existing screen, untouched and still the default; cards mode is the
library presentation running on real bookmarks.

Structure:

- LibraryKit.swift holds what both the shipping screen and the prototype
  draw from — the paper palette, the Bookmark -> LibraryItem projection,
  the color card, and the tab strip. The prototype in Views/Prototypes
  keeps only its standalone chrome and sample data, so the two can no
  longer drift.
- BookmarkActions.swift extracts the context menu and the podcast launch
  path out of BookmarkListRow. Cards and rows now offer exactly the same
  actions because they are the same code. The menu is a @ViewBuilder
  rather than a ViewModifier: each host already owns the sheets it
  presents, and a modifier would have forced a second copy of that state.
- LibraryGridView.swift is the Bookmark-driven grid, with the same tap-to
  -open, long-press-for-menu, pagination and podcast sheets as the list.

Two behaviors worth calling out:

Tag tabs filter server-side through linkding's `#tag` search syntax.
Filtering the loaded page client-side would only ever search the most
recent 50 of 600+ bookmarks and quietly look empty; verified against the
server that `q=#dev-tools` returns 64 tagged results where `q=dev-tools`
returns 0.

Leaving cards mode clears any tag filter. The classic list has no filter
strip to display one, so a filter that survived the switch would be
invisible and the list would look like it had lost bookmarks.

The layout choice persists in @AppStorage. The toolbar button shows the
layout it switches *to* — a two-state toggle labelled with its current
state reads as a status light rather than a control.

Verified on an iPhone 17 Pro simulator against the live linkding server in
both layouts and both color schemes. Full test suite passes (16 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
2026-07-26 01:41:23 -05:00
Krishna Kumar f8693f4be0 Add local source ingest and podcasts 2026-07-02 01:11:03 -05:00
Krishna KumarandClaude Opus 4.8 1f49a7dcbe feat(podcasts): #8 follow-ups — AI summary + in-app Open Bookmark
CI / build-and-deploy (push) Successful in 18s
CI / build-and-deploy (pull_request) Successful in 18s
Resolves the two deviations from the episode detail sheet:

- AI summary now shows in the detail sheet. Added a URL-keyed AISummaryStore
  that BookmarksViewModel populates as it enriches/loads bookmarks, so the
  decoupled Podcasts tab can surface a summary from just the episode URL —
  without recoupling to BookmarksViewModel.
- "Open Bookmark" opens the in-app BrowserView as a nested sheet (generator
  threaded through PodcastLibraryView / EpisodePickerView), alongside the
  existing "Open in Safari" and Share.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:16:16 -05:00
Krishna KumarandClaude Opus 4.8 d2ccbc94b5 feat(podcasts): #6 quick wins — tab badge, remember speed, sleep timer, persist queue
CI / build-and-deploy (push) Successful in 17s
CI / build-and-deploy (pull_request) Successful in 18s
- Unread badge on the Podcasts tab via a new observable PodcastLibrary store
  that mirrors PodcastIndex; all played/remove/generation mutations route
  through it so the count stays live.
- Persist playback speed across sessions (UserDefaults), applied on setup.
- Sleep timer (15/30/45 min or end-of-episode) with a moon menu in the full
  player; end-of-episode stops instead of auto-advancing.
- Persist the current episode + Up Next queue and restore a *paused* session on
  launch; save position when backgrounding. Audio session is only claimed on
  actual playback so a restored session never interrupts other audio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:07:31 -05:00
Krishna KumarandClaude Opus 4.8 af3112530e feat(podcasts): played/unplayed queue, background generation, share-sheet audio
CI / build-and-deploy (push) Successful in 24s
CI / build-and-deploy (pull_request) Successful in 15s
Turns the Podcasts tab into a proper podcast-app experience across three
features that compose on a shared background-generation service.

Background generation (#2)
- New PodcastGenerationManager runs generate→poll→download off the player,
  so producing a new episode never stops current playback.
- Headphone taps never interrupt: idle → generate-and-play in the foreground
  (unchanged); already playing → generate in the background, episode lands in
  the library. A "Generating…" banner surfaces active jobs.

Played/unplayed + queue (#1)
- PodcastEntry gains playedAt (old index.json decodes as unplayed).
- Podcasts tab splits into Up Next / Played with a Play All queue that
  auto-advances; finishing marks played; swipe to toggle played state.

Share-extension audio (#3)
- "Create podcast" toggle on the save card, plus configurable Auto-Podcast
  Tags in Settings. The extension enqueues a request into the App Group; the
  main app drains it on launch/foreground and generates in the background.

Closes #1
Closes #2
Closes #3

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:37:42 -05:00
Krishna KumarandClaude Opus 4.8 96ea5fe6f6 feat(ai): on-device RAG over bookmarks + Spotlight indexing + unified logging
CI / build-and-deploy (push) Successful in 22s
- SpotlightIndexer: actually push BookmarkEntity into the Spotlight index via
  indexAppEntities (IndexedEntity conformance alone indexes nothing). Wired into
  every sync/mutation point. This is what lets Apple Intelligence answer
  free-form Siri questions grounded in the user's bookmarks.
- On-device RAG: BookmarkAssistant (LanguageModelSession) + BookmarkSearchTool
  + SpotlightBookmarkSearch (CSSearchQuery retrieval), surfaced via AskView with
  lightweight Markdown rendering of answers.
- Log: os.Logger facility (com.magicive.marks) replacing ad-hoc print(); shared
  with ShareExtension. ClaudeService now logs server error bodies on non-2xx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:55:32 -05:00
Krishna KumarandClaude Opus 4.8 e82e69c5f3 feat(share): dup-aware interactive save card with AI tag suggestions
CI / build-and-deploy (push) Successful in 20s
Replace the fire-and-forget share toast with an interactive SwiftUI card.
On open it checks linkding's /api/bookmarks/check/ so an already-saved URL
routes to an Update (no more silent duplicates) and a new URL shows a save
form with title, tags, notes, and a read-later toggle.

Tag suggestions are hybrid: fetch the user's existing tag vocabulary
(/api/tags/) and AI tag ideas (/v1/marks/enrich via anonymous device auth),
auto-fill known-vocabulary matches into the field and surface net-new ideas
as tappable chips. Best-effort and non-blocking — failures never block save.

Also:
- Refresh bookmarks when the app returns to the foreground (scenePhase).
- Render bookmark dates as a static relative label instead of the live
  ticking RelativeDateTime timer.
- Share LinkdingAPI/Bookmark/ServerConfig/MarksAuth into the ShareExtension
  target via project.yml (durable across xcodegen regen) and regenerate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:36:46 -05:00
Krishna KumarandClaude Opus 4.8 a3059f25fc feat(intents): onscreen entity annotations + App Intents tests
CI / build-and-deploy (push) Successful in 28s
- bookmarkOnscreen(_:) publishes the viewed bookmark as the on-screen
  entity (NSUserActivity.appEntityIdentifier) so Siri can resolve "this"
  / "summarize this one"; applied to the BrowserView sheets in
  BookmarksView and SearchView
- Add MarksTests (XCTest) covering entity mapping, EntityIdentifier,
  IntentRouter, and the real perform() of SearchMarksIntent and
  OpenBookmarkIntent; wired into the Marks scheme test phase

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:16:46 -05:00
Krishna KumarandClaude Opus 4.8 389d615c8b feat(intents): add App Intents + Siri integration
CI / build-and-deploy (push) Successful in 24s
Expose Marks to Siri, Spotlight, and Shortcuts via App Intents:

- BookmarkEntity (AppEntity + IndexedEntity, keyed on the Linkding
  server id) with a server-backed EntityStringQuery for live lookup
- Intents: Add, Open (OpenIntent), Search, ShowUnread, and an AI
  Summarize intent backed by ClaudeService; AppShortcutsProvider with
  spoken phrases; Siri snippet views and dialog
- SearchMarksIntent conforms to the system.search App Schema
  (@AppIntent(schema:), ShowInAppSearchResultsIntent) so it is
  executable by conversational Siri / Apple Intelligence
- IntentRouter bridges intents (run in the app process) to the SwiftUI
  scene for open/search navigation; widgets refreshed after adds
- Link AppIntents.framework so the metadata processor extracts shortcut
  phrases and validates the assistant schema

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:10:17 -05:00
Krishna KumarandClaude Sonnet 4.6 199fc2d043 Fix ShareExtension "Open Marks first" + add widget + BookmarkListRow polish
CI / build-and-deploy (push) Successful in 41s
- Save serverConfig to App Group on every app launch so ShareExtension
  can always find credentials without requiring a disconnect/reconnect flow
- Add MarksWidget target with RandomBookmarkWidget (random saved bookmark,
  reloads hourly) using file-based App Group storage to avoid CFPreferences issues
- Extract BookmarkListRow as shared component with editorial typography tweaks
  (17pt semibold title, 13pt domain, relative date, 32×32 favicon, italic AI summary,
  20pt podcast icon, 16pt horizontal insets)
- Add MarksWidget and ShareExtension Xcode schemes for direct run/debug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:11:05 -05:00
Krishna KumarandClaude Sonnet 4.6 18423a7633 Add iOS Langfuse analytics via backend events endpoint
CI / build-and-deploy (push) Failing after 8s
AnalyticsService.track() is fire-and-forget — posts to POST /v1/marks/events
which records a Langfuse trace. No external SDK, no Sentry on iOS side.

Events tracked:
- podcast.started (url, cached)
- podcast.completed (url, durationSec) — fires at 97% playback
- podcast.failed (url, error)
- search.semantic (query, resultCount)
- enrich.completed (count)
- collections.generated (collectionCount)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:30:03 -05:00
Krishna KumarandClaude Sonnet 4.6 38cd72416c Xcode project sync and seek async fix
CI / build-and-deploy (push) Successful in 23s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 16:59:22 -05:00
Krishna KumarandClaude Sonnet 4.6 bc16ee986d Migrate AI features to backend, add podcast generation
- ClaudeService: replace direct OpenRouter calls with backend endpoints
  (/v1/marks/enrich, /v1/marks/search, /v1/marks/collections)
- ClaudeService: add podcast methods (generate, status, downloadAudio)
- SettingsView: replace OpenRouter key field with backend URL + API key
- PodcastPlayerView: new sheet — generate → poll → AVPlayer playback
- BrowserView: headphones toolbar button triggers podcast for current URL
- BookmarksView: "Convert to Podcast" context menu item + sheet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 18:39:15 -05:00
Krishna KumarandClaude Sonnet 4.6 ac2f9cf85d Add in-app browser with reader mode
CI / build-and-deploy (push) Successful in 22s
Tapping a bookmark now opens an in-app WKWebView instead of bouncing to
Safari. Reader mode injects JS to extract article content and rerender
with clean serif typography and dark-mode support. Bottom toolbar has
back/forward, reader toggle, share, and open-in-Safari escape hatch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 12:43:33 -05:00
Krishna KumarandClaude Sonnet 4.6 1b40f50aab Add edit bookmark, offline cache, and unread filter
- Edit bookmark: full PATCH via swipe action or long-press context menu;
  EditBookmarkView covers URL, title, description, tags, and unread toggle
- Offline cache: bookmarks persisted to ApplicationSupport JSON and shown
  instantly on launch; cache is per-server, skipped when unread filter is on
- Unread filter: toolbar toggle sends ?unread=true to the API; title updates
  to "Unread"; no stale-cache flash when toggling
- Extract normalizedURL and parsedTags into String+Helpers, removing three
  copies of URL normalization and two of tag parsing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 11:51:54 -05:00
Krishna KumarandClaude Sonnet 4.6 b7ef5c2215 Replace Flutter app with native SwiftUI rewrite (iOS 26)
Complete ground-up rewrite in SwiftUI targeting iOS 26. Drops the Flutter/Dart
codebase entirely in favour of a lean native app with no third-party dependencies.

Features shipped:
- Bookmark list with pagination, pull-to-refresh, swipe delete/archive
- Add bookmark form + iOS share extension (zero-tap save from any app)
- Tags tab — all tags sorted by count, tap to browse filtered bookmarks
- Native search tab (Tab role: .search) with instant client-side filtering
- AI enrichment via OpenRouter (google/gemini-2.0-flash-lite-001): auto-summary
  and tag generation, semantic search, smart collections
- Settings: linkding server config, OpenRouter API key
- App Groups for credential sharing between main app and share extension
- Swift 6 strict concurrency throughout (@Observable, @MainActor)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 08:33:01 -05:00