Skip to content

feat(mobile): web search + sync/folder/delete fixes - #87

Merged
ddutchie merged 9 commits into
mainfrom
ddutchie/indexandmobilepdf
Jul 18, 2026
Merged

ddutchie merged 9 commits into
mainfrom
ddutchie/indexandmobilepdf

Conversation

@ddutchie

@ddutchie ddutchie commented Jul 18, 2026 •

Copy link
Copy Markdown
Owner

What does this PR do?

Adds mobile web search/extract to chat and fixes a cluster of sync + on-disk data-integrity bugs surfaced while testing desktop↔mobile sync. Also carries earlier mobile work already on this branch (PDF export, codebase reindex-on-parser-upgrade, web-view reload after iOS content-process termination).

Type of change

  • Bug fix
  • New feature
  • Refactor / code quality
  • Docs / changelog
  • Tests

Highlights

Mobile — web search/extract (feat)

  • New web_search and web_extract chat tools; user-selectable Tavily or Brave provider (Brave is search-only, extraction falls back to a Tavily key). Keys stored in expo-secure-store, entered via a new Web search section in AI settings.

Notes / folders (fix)

  • Whitespace/slash-only folder values collapse to root (no more phantom "unnamed" folder); buildFolderTree renders pre-existing bad rows at root.
  • Renaming a project now relocates its on-disk notes directory (renderer IPC + MCP upsert_project), merging safely if the target exists; reconcileProjectFolders() heals workspaces renamed before this fix.

Sync (fix) — the core of this PR

  • Stop syncing chat. chat_threads/chat_messages were wrongly in SYNCABLE_TABLES, replicating every AI conversation (≈79% of the oplog) and re-processing hundreds of ops per sync. Removed from the synced set; migrations desktop v28 / mobile v4 drop the chat capture triggers and purge chat from the oplog. applyRemote now ignores ops for non-synced entities so an out-of-date peer can't re-inject them.
  • Never make a conflict copy of a conflict copy (guard on _conflict_ ids); boot tombstones existing nested-conflict junk through the engine so it converges on both devices.
  • Deletes converge in one pass. Synced-deleted notes are kept as tombstones (physical delete made every peer delete re-apply/re-stage — the "N pending never settles" loop); desktop reads (getNotes/getCards/searchNotes/searchTasks) now filter deleted_at, so deletes hide in the UI. This also fixes "deleted on mobile still shows on desktop".
  • Array-merge rows only re-stamp when the union changes the local row, so already-merged tag/link lists stop re-exchanging every sync.
  • Corrected the sync log (actual applied count vs peer ops read; converged syncs are now silent) and added a sync:pendingBreakdown diagnostic.

Screenshots / recording

UI change is the new Web search section in mobile AI settings (provider segment + secure key fields). Screenshot to be added.
Screenshot 2026-07-18 at 10 35 36 AM

Checklist

  • npm run type-check:all passes (+ mobile tsc --noEmit)
  • npm run lint passes
  • npm test passes (812 tests green across shared/ + electron/)
  • npm run test:e2e passes (not run — no desktop UI surface changed here)
  • No hardcoded colours — CSS variables only
  • No text-[Npx] pixel font classes
  • New IPC handlers wrapped in handle() and return IpcResult<T> (sync:pendingBreakdown)
  • New DB migrations appended (not edited) — desktop v28 in schema.ts, mobile v4 in mobile/src/db/migrations.ts
  • New SQL goes in electron/db/queries.ts
  • New dependencies/devDependencies — none added (mobile web tools use expo/fetch, no new deps)
  • New MCP tools — none added (mobile chat tools are separate from the MCP tool registry)
  • --external:<pkg> flags — unchanged

Notes for reviewer

  • Both apps must be rebuilt/reinstalled for the sync fixes to fully take effect: mobile migration v4 (drop chat triggers + purge chat oplog) runs on reinstall; desktop v28 + engine fixes need npm run compile + restart. The applyRemote non-syncable-entity guard means the desktop is protected even before mobile is updated.
  • Root cause of the "N pending / applied=1802" reports was diagnosed from the live iCloud oplog (chat dominated the file); the recurring applied=384 afterward turned out to be a mislabeled log (peer ops read vs applied) — now corrected, with the engine verified idempotent.
  • content_text is still synced redundantly in note payloads (~30% of note bytes); intentionally left as-is (stripping it needs a wire-format change + receiver re-derivation) — happy to follow up if desired.
  • This branch also contains 4 pre-existing commits (PDF export ×2, codebase reindex, web-view reload) that predate this session.

Summary by CodeRabbit

  • New Features
    • Added mobile web search with Tavily or Brave, secure API key storage, source extraction, and citations in AI chat.
    • Added “Export as PDF” for notes with light and dark styling.
    • Added optional forced full codebase re-indexing.
  • Bug Fixes
    • Improved sync reliability, including cleaner deletion handling and fewer duplicate conflict copies.
    • Chat history remains local and is no longer synchronized across devices.
    • Fixed project renames, misplaced folders, stale search results, and blank knowledge graphs after WebView interruptions.
  • Documentation
    • Updated release notes for desktop and mobile improvements.

ddutchie added 8 commits July 15, 2026 17:59
Real bug surfaced by dogfooding: codebase_reindex caches each file by a
content hash (${size}-${mtimeMs}). After the indexer binary was upgraded
to capture const/type/interface/enum, files whose on-disk content was
unchanged kept their OLD symbols, and calling codebase_reindex from MCP
silently no-oped them (only a full rebuild via a different path fixed it).

- Fold a PARSER_VERSION into the per-file hash so any extraction-logic
  change invalidates every previously-indexed file on the next reindex.
  Bumped to v2 for the const/type/enum work.
- Add an optional force flag to indexCodebase + the codebase_reindex tool
  (schema + executor) that drops the root's cached rows and re-parses
  everything, as an explicit escape hatch.

Test: force:true re-parses a file whose symbols were wiped without a
content change (a plain reindex correctly skips it). type-check + 1111
node tests + eslint --max-warnings 0 + compile green. Changelog v2.5.1.
Adds an 'Export as PDF' action to the note menu, mirroring desktop PDF
export — a beautifully formatted PDF you can print, save to Files, or
share.

- shared/notes/pdf-template.ts: the desktop PDF HTML template moved to
  shared (pure, no Electron deps); electron/lib/pdf-template.ts is now a
  re-export shim so both platforms build the same document.
- shared/notes/note-html.ts: pure helpers converting Cairn callouts
  (> [!type]) and [[wikilinks]] into the exact [data-callout] / chip HTML
  the template styles — so mobile PDFs match desktop, incl. callouts +
  wikilinks. Unit-tested.
- mobile/src/notes/note-pdf.ts: renders note markdown -> HTML (markdown-it
  + markdown-it-mark + the shared callout/wikilink passes) -> buildPdfHtml
  -> expo-print printToFileAsync -> share sheet (follows light/dark).
- Wired an Export as PDF menu action (ICON_PDF) into NoteDetailScreen.
- deps: added expo-print + expo-sharing (SDK-57 pinned); bumped safe
  JS-only libs (ai, lucide-react-native). NOTE: removed the expo-sharing
  config plugin from app.json — its plugin scaffolds an unwanted iOS share
  extension; shareAsync needs no plugin (this caused a launch crash).

Verify: expo-doctor clean, mobile tsc + eslint clean, shared tests green,
desktop compile OK. Changelog mobile v0.1.3.
expo-print writes to a random cache filename, so shared/saved PDFs had an
opaque name. After printing, move the file to "<Title>.pdf" in the cache
dir before sharing, using a new shared pdfSafeFilename() that strips
filesystem-illegal characters (/ \ : * ? " < > |) and falls back to
'untitled' — mirroring the desktop sanitizeFilename/pdfSafeTitle.

- shared/notes/pdf-template.ts: add pdfSafeFilename() (+ tests, plus a
  buildPdfHtml smoke test the shared template lacked).
- mobile note-pdf: move printToFileAsync output to the titled file via
  expo-file-system File.moveSync; best-effort (falls back to the original
  uri if the move fails), and deletes any stale same-named file first.

type-check:all + shared tests + lint clean.
The Knowledge Graph could go blank after the app was backgrounded for a
while, with only an app restart fixing it. Root cause: iOS terminates the
WKWebView content process under background memory pressure, leaving a blank
view; the graph pushes live updates via injectJavaScript (which silently
no-ops on a dead process) and only regenerates its HTML source on
structural changes, so returning to the tab never recovered it.

Add onContentProcessDidTerminate (iOS) + onRenderProcessGone (Android)
handlers that reload() the WebView:
- KnowledgeGraphWebView: reload rebuilds the D3 document.
- WebViewRenderer (math/diagram blocks in notes): reload + reset loading
  spinner so KaTeX/Mermaid re-render.

Changelog mobile v0.1.3.
Adds web_search and web_extract agent tools so the mobile assistant can
answer questions beyond the user's notes/tasks. Both providers are
user-selectable; Tavily covers search + page extraction, Brave covers
search only (extraction falls back to a Tavily key). API keys are stored
in expo-secure-store, entered via a new Web search section in AI settings.
- Collapse whitespace/slash-only folder values to root so notes can no
  longer land in a blank "unnamed" folder; buildFolderTree renders any
  pre-existing bad rows at root too.
- Rename a project's on-disk notes directory when the project is renamed
  (both the renderer IPC and the MCP upsert_project paths), merging safely
  if a folder already exists at the new name.
- reconcileProjectFolders() heals workspaces where a rename happened before
  this fix by matching each dir's notes to their project via frontmatter.
The sync oplog was dominated by chat and never settled ("N pending" /
recurring applied counts). Root causes fixed:

- Stop syncing chat_threads/chat_messages — chat is device-local and
  online-only, but was in SYNCABLE_TABLES, replicating every conversation
  (79% of the oplog). Remove from the synced set; migrations (desktop v28,
  mobile v4) drop the chat capture triggers and purge chat from the oplog.
  applyRemote now ignores ops for non-synced entities so an out-of-date
  peer can't re-inject them.
- Never make a conflict copy of a conflict copy (guard on _conflict_ ids);
  boot tombstones existing nested-conflict junk THROUGH the engine so it
  converges on both devices.
- Keep synced-deleted notes as tombstones instead of physically removing
  them (physical delete made every peer delete re-apply and re-stage); the
  desktop reads (getNotes/getCards/searchNotes/searchTasks) now filter
  deleted_at, so deletes hide in the UI and converge in one pass.
- Only re-stamp array-merge rows when the union changes the LOCAL row, so
  already-merged tag/link lists stop being re-exchanged every sync.

Also: correct the sync log to report the actual applied count vs peer ops
read (converged syncs are now silent), and add a sync:pendingBreakdown
diagnostic.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ddutchie, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 375afd55-2b6c-4626-a0d5-e3feb29898d8

📥 Commits

Reviewing files that changed from the base of the PR and between 21c320c and 581e4a6.

📒 Files selected for processing (8)
  • changelogs/v2.5.1.md
  • electron/main.ts
  • mobile/src/components/AiSettingsForm.tsx
  • mobile/src/notes/note-pdf.ts
  • shared/notes/note-html.test.ts
  • shared/notes/note-html.ts
  • shared/sync/engine.test.ts
  • shared/sync/engine.ts
📝 Walkthrough

Walkthrough

This release updates sync convergence and tombstone handling, relocates renamed project folders, adds mobile web search and PDF export, recovers terminated WebViews, normalizes note folders, and adds parser-versioned or forced codebase reindexing.

Changes

Synchronization and note maintenance

Layer / File(s) Summary
Sync convergence and entity filtering
shared/sync/*
Excludes chat from sync, ignores unsupported remote entities, prevents nested conflict copies, and stops redundant array-union restamping.
Tombstone and folder cleanup
electron/db/*, electron/main.ts, electron/splash/*, electron/notes-files.ts, electron/shared/notes-io.ts
Hides tombstoned records, removes orphaned files, cleans nested conflict copies, and reconciles renamed project directories.
Sync diagnostics
electron/sync/*, electron/preload.ts
Reports peer operation counts and exposes grouped pending-sync diagnostics.

Mobile web search and PDF export

Layer / File(s) Summary
Web-assisted chat
mobile/src/chat/*, mobile/src/components/AiSettingsForm.tsx
Adds Tavily and Brave configuration, secure API-key storage, web search/extraction tools, and agent instructions for cited external results.
Note PDF export
shared/notes/*, mobile/src/notes/*, mobile/src/screens/NoteDetailScreen.tsx, electron/lib/pdf-template.ts
Adds shared HTML/PDF helpers and a mobile export-and-share flow with light or dark themes.
WebView recovery and mobile updates
mobile/src/components/*, mobile/app.json, mobile/package.json
Reloads terminated WebViews and updates Expo configuration, dependencies, and changelog entries.

Indexing and folder normalization

Layer / File(s) Summary
Forced and versioned reindexing
electron/lib/codebase-index.ts, electron/lib/tool-schemas.ts, electron/mcp/tools/*
Invalidates parser-versioned caches and adds force support to full codebase reindexing.
Canonical folder paths
shared/notes/folder-tree.ts, mobile/src/db/queries.ts, related tests
Normalizes whitespace-, empty-, and slash-only folders to the project root at tree-building and write boundaries.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MobileChat
  participant WebTools
  participant SearchProvider
  participant ExtractionProvider
  MobileChat->>WebTools: invoke web_search
  WebTools->>SearchProvider: request provider search
  SearchProvider-->>WebTools: return compact results
  MobileChat->>WebTools: invoke web_extract
  WebTools->>ExtractionProvider: request page content
  ExtractionProvider-->>WebTools: return bounded markdown
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the main themes: mobile web search plus sync, folder, and delete fixes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ddutchie/indexandmobilepdf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
mobile/src/components/AiSettingsForm.tsx (1)

141-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Await all secure-store keys before exposing the form.

Currently, setLoading(false) fires as soon as the OpenAI key resolves, while the Tavily and Brave keys load independently in parallel. This can cause the web search API key fields to briefly appear blank before popping in a moment after the form renders. Using Promise.all guarantees all keys are fully resolved before dropping the loading state.

♻️ Proposed refactor to use `Promise.all`
   useEffect(() => {
     let cancelled = false;
-    getOpenAIApiKey().then((k) => {
-      if (cancelled) return;
-      setHadKey(k != null);
-      setApiKey(k ?? "");
-      setLoadedKey(k ?? "");
-      setLoading(false);
-    });
-    // Web-search keys (secure store) load in parallel; failures leave them blank.
-    getTavilyApiKey().then((k) => {
-      if (cancelled) return;
-      setHadTavilyKey(k != null);
-      setTavilyKey(k ?? "");
-      setLoadedTavilyKey(k ?? "");
-    });
-    getBraveApiKey().then((k) => {
-      if (cancelled) return;
-      setHadBraveKey(k != null);
-      setBraveKey(k ?? "");
-      setLoadedBraveKey(k ?? "");
-    });
+    Promise.all([
+      getOpenAIApiKey(),
+      getTavilyApiKey(),
+      getBraveApiKey()
+    ]).then(([openAI, tavily, brave]) => {
+      if (cancelled) return;
+      
+      setHadKey(openAI != null);
+      setApiKey(openAI ?? "");
+      setLoadedKey(openAI ?? "");
+      
+      setHadTavilyKey(tavily != null);
+      setTavilyKey(tavily ?? "");
+      setLoadedTavilyKey(tavily ?? "");
+      
+      setHadBraveKey(brave != null);
+      setBraveKey(brave ?? "");
+      setLoadedBraveKey(brave ?? "");
+      
+      setLoading(false);
+    });
     return () => {
       cancelled = true;
     };
   }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mobile/src/components/AiSettingsForm.tsx` around lines 141 - 166, Update the
AiSettingsForm useEffect to load the OpenAI, Tavily, and Brave keys via
Promise.all, apply each resolved key to its corresponding state, and call
setLoading(false) only after all three secure-store requests complete. Preserve
the cancelled guard so unmounted components do not receive updates.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/main.ts`:
- Around line 475-500: The nested conflict-copy cleanup currently runs before
the initial sync import, so peer-only junk is missed. Move the cleanup block
identified by findNestedConflictCopies and tombstoneNotesViaSync to execute
after connected sync operations, and repeat or recheck it until no candidates
remain so imported peer junk is converged during the current session.

In `@mobile/src/notes/note-pdf.ts`:
- Around line 33-40: Update the note rendering flow around calloutsToHtml,
wikilinksToChips, and md.render so Cairn transformations skip fenced and inline
code content. Implement the transformations as Markdown-It rules/plugins or
protect code tokens before applying them, while preserving callout and wikilink
conversion in ordinary Markdown text.
- Around line 77-86: Update the unavailable-sharing branch in the note PDF
function around Sharing.isAvailableAsync() so it does not silently return a
successful result without access to the generated file. Include shareUri in the
returned result for the caller to use, or report sharing as unavailable through
the function’s established error/result mechanism; preserve the successful
shared response when Sharing.shareAsync completes.
- Line 28: Sanitize note-authored HTML in the MarkdownIt-to-PDF flow around the
MarkdownIt configuration and export logic, using a strict allowlist or custom
token/plugin path that preserves generated callout and chip fragments while
removing all other raw HTML before it reaches Expo Print.

In `@shared/notes/note-html.ts`:
- Around line 69-71: Update the body rendering flow around wikilinksToChips so
ordinary text and wikilink title captures are escaped exactly once; do not pass
an already escaped full line into wikilinksToChips. Preserve callout line-break
rendering, and add a regression test covering a title such as [[a<b>]] that must
render with the expected single escaping behavior.

In `@shared/sync/engine.ts`:
- Around line 540-555: Update the re-stamping logic around
unionChangedBeyondRemote and unionChangedBeyondLocal so an authoritative merged
payload is preserved or published whenever merged contains union elements
missing from the winning higher-HLC remote payload, even when it equals local.
Ensure recordForwardedOp and compactOplog retain the complete union while
preventing repeated publication once that merged state has propagated. Add
coverage for a later-HLC scalar edit carrying an older array subset.

---

Nitpick comments:
In `@mobile/src/components/AiSettingsForm.tsx`:
- Around line 141-166: Update the AiSettingsForm useEffect to load the OpenAI,
Tavily, and Brave keys via Promise.all, apply each resolved key to its
corresponding state, and call setLoading(false) only after all three
secure-store requests complete. Preserve the cancelled guard so unmounted
components do not receive updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee2589ff-9f78-401b-82e0-44987d578f90

📥 Commits

Reviewing files that changed from the base of the PR and between 5db7b11 and 21c320c.

⛔ Files ignored due to path filters (1)
  • mobile/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (45)
  • changelogs/v2.5.1.md
  • electron/db/queries.test.ts
  • electron/db/queries.ts
  • electron/db/schema.ts
  • electron/ipc/db-handlers.ts
  • electron/lib/codebase-index.test.ts
  • electron/lib/codebase-index.ts
  • electron/lib/pdf-template.ts
  • electron/lib/tool-schemas.ts
  • electron/main.ts
  • electron/mcp/tools/codebase.ts
  • electron/mcp/tools/index.ts
  • electron/mcp/tools/projects.ts
  • electron/notes-files.test.ts
  • electron/notes-files.ts
  • electron/preload.ts
  • electron/shared/notes-io.ts
  • electron/splash/boot-sequence.ts
  • electron/sync/desktop-sync.ts
  • electron/sync/sync-handlers.ts
  • mobile/app.json
  • mobile/changelogs/v0.1.3.md
  • mobile/package.json
  • mobile/src/chat/agent.ts
  • mobile/src/chat/tools.ts
  • mobile/src/chat/web-config.ts
  • mobile/src/chat/web-tools.ts
  • mobile/src/components/AiSettingsForm.tsx
  • mobile/src/components/KnowledgeGraphWebView.tsx
  • mobile/src/components/WebViewRenderer.tsx
  • mobile/src/components/toolbar-icons.ts
  • mobile/src/db/migrations.ts
  • mobile/src/db/queries.ts
  • mobile/src/notes/note-pdf.ts
  • mobile/src/screens/NoteDetailScreen.tsx
  • shared/notes/folder-tree.test.ts
  • shared/notes/folder-tree.ts
  • shared/notes/note-html.test.ts
  • shared/notes/note-html.ts
  • shared/notes/pdf-template.test.ts
  • shared/notes/pdf-template.ts
  • shared/package.json
  • shared/sync/engine.test.ts
  • shared/sync/engine.ts
  • shared/sync/schema.ts

Comment thread electron/main.ts Outdated
Comment thread mobile/src/notes/note-pdf.ts Outdated
Comment thread mobile/src/notes/note-pdf.ts Outdated
Comment thread mobile/src/notes/note-pdf.ts Outdated
Comment thread shared/notes/note-html.ts
Comment thread shared/sync/engine.ts Outdated
…ming

- sync: always republish the full array-union even when the winning remote
  carried an older subset, so no tag/link addition is silently dropped
  (the previous "only re-stamp on local change" gate could diverge peers);
  still converges without re-exchange. Add coverage for a later-HLC scalar
  edit shipping a stale array subset.
- main: run the nested conflict-copy cleanup AFTER a sync imports peer ops
  (looping until none remain), not only at startup, so peer-only junk
  converges this session; publish the resulting tombstones.
- mobile PDF: render with markdown-it html:false and inject only our own
  trusted callout/chip fragments via sentinels, so user-authored raw HTML
  is neutralized; apply Cairn transforms only outside fenced/inline code
  (transformOutsideCode + code-aware calloutsToHtml).
- mobile PDF: return the file uri when system sharing is unavailable so the
  caller has a handle instead of a bare success.
- note-html: single-pass escapeAndChipWikilinks so a callout-body wikilink
  title with HTML (e.g. [[a<b>]]) is escaped exactly once, not twice.
- AiSettingsForm: load OpenAI/Tavily/Brave keys via Promise.all and clear
  loading only after all three resolve (keep the cancelled guard).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant