Repository navigation
feat(mobile): web search + sync/folder/delete fixes - #87
Conversation
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.
|
Warning Review limit reached
Next review available in: 36 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughThis 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. ChangesSynchronization and note maintenance
Mobile web search and PDF export
Indexing and folder normalization
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
mobile/src/components/AiSettingsForm.tsx (1)
141-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAwait 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. UsingPromise.allguarantees 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
⛔ Files ignored due to path filters (1)
mobile/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (45)
changelogs/v2.5.1.mdelectron/db/queries.test.tselectron/db/queries.tselectron/db/schema.tselectron/ipc/db-handlers.tselectron/lib/codebase-index.test.tselectron/lib/codebase-index.tselectron/lib/pdf-template.tselectron/lib/tool-schemas.tselectron/main.tselectron/mcp/tools/codebase.tselectron/mcp/tools/index.tselectron/mcp/tools/projects.tselectron/notes-files.test.tselectron/notes-files.tselectron/preload.tselectron/shared/notes-io.tselectron/splash/boot-sequence.tselectron/sync/desktop-sync.tselectron/sync/sync-handlers.tsmobile/app.jsonmobile/changelogs/v0.1.3.mdmobile/package.jsonmobile/src/chat/agent.tsmobile/src/chat/tools.tsmobile/src/chat/web-config.tsmobile/src/chat/web-tools.tsmobile/src/components/AiSettingsForm.tsxmobile/src/components/KnowledgeGraphWebView.tsxmobile/src/components/WebViewRenderer.tsxmobile/src/components/toolbar-icons.tsmobile/src/db/migrations.tsmobile/src/db/queries.tsmobile/src/notes/note-pdf.tsmobile/src/screens/NoteDetailScreen.tsxshared/notes/folder-tree.test.tsshared/notes/folder-tree.tsshared/notes/note-html.test.tsshared/notes/note-html.tsshared/notes/pdf-template.test.tsshared/notes/pdf-template.tsshared/package.jsonshared/sync/engine.test.tsshared/sync/engine.tsshared/sync/schema.ts
…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).
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
Highlights
Mobile — web search/extract (
feat)web_searchandweb_extractchat tools; user-selectable Tavily or Brave provider (Brave is search-only, extraction falls back to a Tavily key). Keys stored inexpo-secure-store, entered via a new Web search section in AI settings.Notes / folders (
fix)buildFolderTreerenders pre-existing bad rows at root.upsert_project), merging safely if the target exists;reconcileProjectFolders()heals workspaces renamed before this fix.Sync (
fix) — the core of this PRchat_threads/chat_messageswere wrongly inSYNCABLE_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.applyRemotenow ignores ops for non-synced entities so an out-of-date peer can't re-inject them._conflict_ids); boot tombstones existing nested-conflict junk through the engine so it converges on both devices.getNotes/getCards/searchNotes/searchTasks) now filterdeleted_at, so deletes hide in the UI. This also fixes "deleted on mobile still shows on desktop".sync:pendingBreakdowndiagnostic.Screenshots / recording
UI change is the new Web search section in mobile AI settings (provider segment + secure key fields). Screenshot to be added.

Checklist
npm run type-check:allpasses (+ mobiletsc --noEmit)npm run lintpassesnpm testpasses (812 tests green acrossshared/+electron/)npm run test:e2epasses (not run — no desktop UI surface changed here)text-[Npx]pixel font classeshandle()and returnIpcResult<T>(sync:pendingBreakdown)schema.ts, mobile v4 inmobile/src/db/migrations.tselectron/db/queries.tsdependencies/devDependencies— none added (mobile web tools useexpo/fetch, no new deps)--external:<pkg>flags — unchangedNotes for reviewer
npm run compile+ restart. TheapplyRemotenon-syncable-entity guard means the desktop is protected even before mobile is updated.applied=384afterward turned out to be a mislabeled log (peer ops read vs applied) — now corrected, with the engine verified idempotent.content_textis 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.Summary by CodeRabbit