Repository navigation
Shared-logic extraction, mobile component reuse, and desktop bug fixes - #81
Conversation
…card - Remove the `title="Workspaces"` prop from the header workspace menu. On some renders the native bar-button `title` bled into the header title slot, intermittently showing "Workspaces" instead of the Cairn icon. The menu is now purely icon-only; the active workspace name still shows as the centered header title. - Remove the workspace switcher card from the Sync modal — it duplicated the header switcher now that WorkspaceHeaderMenu exists. The Sync modal is now purely sync status/diagnostics (iCloud availability, last result, conflicts). Drops the now-dead source scan/switch state.
Dedupe three patterns found across the mobile app (no behaviour change):
- type.overline token + <SectionLabel>: consolidate the ad-hoc uppercase
tracked section/card label style (which had drifted to 11–12px, 600/700
weight, 0.4/0.5 tracking across 8 sites) into one theme token that
scales with the OS text-size setting.
- useRefreshOnFocus(load): replace the copy-pasted
`useFocusEffect(useCallback(() => load(), [load])); useDataChanged(load);`
pair in 7 screens with one hook.
- useSyncSources({ mergeActive?, gateOnICloud? }): share the folder-scan +
listSources round-trip between the first-run SourcePicker (iCloud-gated,
error-surfacing) and the header WorkspaceHeaderMenu (active-merged,
name-sorted).
Second dedupe pass (no behaviour change): - PriorityChips/ColumnChips (src/components/TaskChips.tsx) + a shared PRIORITIES/Priority export from shared/ui/constants: the identical priority + column pill rows in the new-task composer and card detail screen now render from one place. - ResultRow (src/components/ResultRow.tsx): the Search tab's three result lists and the note detail "Related notes" list shared the same surface+border card with a title / optional preview / optional percent-score / optional priority dot — now one component. - NoteEditorBody (src/components/NoteEditorBody.tsx): the Markdown editing surface (keyboard-aware scroll + title/body inputs + sticky toolbar + wikilink sheet) shared by the new-note composer and note detail's edit mode. - NotFound (src/components/NotFound.tsx): the centered "<X> not found" placeholder shared by the note and card detail screens. Skipped the proposed SegmentedControl and EmptyState migrations: the three segmented controls differ meaningfully (glass/sticky vs icon+check vs count-badge) and the hand-rolled empty states use contextual icons — sharing them would add prop complexity or regress visuals for little gain.
…bile
Both apps had byte-identical copies of the embeddings section-splitter
and the vector-similarity math — logic that MUST stay in lockstep or
desktop/mobile semantic search would drift. Extract them to shared/:
- shared/notes/sections.ts — splitIntoSections/NoteSection (was
duplicated in electron/embeddings/sections.ts and
mobile/src/notes/embeddings.ts). Test suite moved to
shared/notes/sections.test.ts.
- shared/embeddings/vector.ts — cosine/topK/dotNormalized/magnitude/
toFloat32 (mobile's local `dot` == desktop's dotNormalized). Test suite
moved to shared/embeddings/vector.test.ts.
electron/embeddings/{sections,cosine}.ts become thin re-export shims so
existing relative importers and the desktop test/bench suites keep
working. Mobile imports the shared modules via @cairn/shared/*.
Drop `rootDir: "electron"` from tsconfig.electron.json so electron files
(which are type-checked with --noEmit; esbuild does the real build) can
import top-level shared/ without the rootDir error — the same cross-import
electron/sync/ already relies on.
Wikilink bug: rehypeEscapeUnknownTags (added in 201cd17 to escape unknown <angle-bracket> tags) had an allow-list that omitted "wikilink", so every <wikilink> element from remarkWikilinks was serialized back to literal text (`<wikilink dataTitle="...">`) before ReactMarkdown could map it to the clickable component. Add "wikilink" to KNOWN_HTML_TAGS. Perf: the markdown transform pipeline is fast (~23ms on the large fixture, incl. rehypeRaw). The real cost was React re-rendering — the ReactMarkdown `components` map depended on the whole `notes` array and `note.content`, so any note edit / sync tick anywhere rebuilt the map and forced a full re-parse + KaTeX re-mount of the open note. Resolve wikilinks/links via a notesRef and checkbox toggles via a noteContentRef so the component map depends only on [toggleCheckbox] (stable except on note switch). Tests: add src/lib/markdown/pipeline.test.ts (guards that wikilinks + callouts survive the escape pass; verified it fails without the fix), and correct the markdown-pipeline benchmark — its "full pipeline" omitted rehypeRaw, rehypeEscapeUnknownTags, remarkObsidianEmbeds and remarkWikilinks; added isolated benches for those four stages.
Hand-rolled overlays bound their Escape handler to the <input> element, so Escape only closed them while the field was focused — clicking a result, the backdrop, or a filter chip moved focus off the input and Escape did nothing. Add a shared useEscapeKey(onEscape, enabled) hook that registers a document-level (capture-phase) keydown listener, and adopt it in: - SearchPanel — the reported bug (Escape was on the search input) - DatePicker — had NO Escape handler at all (closed on outside-click only) - NoteTagBar (BacklinksPanel) tag picker — replaces its input-bound Escape Capture-phase + stopPropagation means an open picker nested inside a Radix dialog closes the picker only (not the dialog) on the first Escape. Radix Dialog/ModalShell modals already handle document-level Escape, so they're unaffected.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThis PR consolidates shared indexing utilities, adds mobile overview and calendar drag-and-drop flows, introduces reusable UI and synchronization hooks, expands graph and workspace navigation, supports note/card wikilinks, stabilizes note preview rendering, and standardizes Escape-key handling. ChangesCross-platform behavior and UI consolidation
Estimated code review effort: 4 (Complex) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant Screen
participant useRefreshOnFocus
participant SyncStatus
Screen->>useRefreshOnFocus: register load callback
useRefreshOnFocus->>Screen: call load on focus
SyncStatus->>useRefreshOnFocus: notify inbound data change
useRefreshOnFocus->>Screen: reload data
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
mobile/app/(tabs)/chat/index.tsxESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox. mobile/src/components/MarkdownView.tsxESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox. mobile/src/db/chat-store.tsESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.
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.
🧹 Nitpick comments (3)
mobile/app/sync.tsx (1)
34-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider reusing the new shared refresh hook here too.
This screen still wires iCloud availability refresh manually via
useFocusEffect(useCallback(() => refresh(), [refresh])), whileconflicts.tsxin the same PR was migrated to the shareduseRefreshOnFocushook. For consistency (and to also pick up on sync-data-changed events, not just focus), consider switching this touseRefreshOnFocus(refresh).♻️ Optional consolidation
- useFocusEffect(useCallback(() => refresh(), [refresh])); + useRefreshOnFocus(refresh);🤖 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/app/sync.tsx` around lines 34 - 38, Replace the manual useFocusEffect wrapper in the sync screen with the shared useRefreshOnFocus(refresh) hook, reusing the existing refresh callback that calls iCloudAvailable().then(setAvailable).catch(() => setAvailable(false)).src/lib/markdown/pipeline.test.ts (1)
70-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider adding a regression test for raw HTML
<mark>tags.
markwas added toKNOWN_HTML_TAGSto allow raw HTML<mark>tags to survive the escape pass (the==highlight==marks created byrehypeMergedPassrun after the escape pass, so they don't need the allowlist entry). A test would guard againstmarkbeing accidentally removed from the allowlist in the future.✨ Suggested test
it("keeps callouts as <callout> elements", () => { const tree = toHast("> [!note] Heads up\n> body"); expect(tagNames(tree)).toContain("callout"); expect(allText(tree)).not.toContain("<callout"); }); + + it("keeps raw HTML <mark> tags as elements", () => { + const tree = toHast("Highlight <mark>text</mark> here."); + expect(tagNames(tree)).toContain("mark"); + expect(allText(tree)).not.toContain("<mark>"); + });🤖 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 `@src/lib/markdown/pipeline.test.ts` around lines 70 - 74, Add a regression test alongside the existing callout test that passes raw HTML containing a <mark> element through the markdown pipeline and asserts the resulting tree retains a mark element, verifying the KNOWN_HTML_TAGS allowlist behavior.src/hooks/useEscapeKey.ts (1)
17-28: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider stabilizing
onEscapewith a ref to avoid listener churn.
onEscapeis in theuseEffectdependency array. Two of three consumers (date-picker.tsxline 45 andBacklinksPanel.tsxline 322) pass inline arrow functions, so every re-render detaches and re-attaches the document listener. A ref pattern decouples listener lifetime from callback identity:♻️ Proposed refactor
export function useEscapeKey(onEscape: () => void, enabled = true): void { + const onEscapeRef = useRef(onEscape); + useEffect(() => { onEscapeRef.current = onEscape; }); + useEffect(() => { if (!enabled) return; const handler = (e: KeyboardEvent) => { if (e.key !== "Escape") return; e.stopPropagation(); - onEscape(); + onEscapeRef.current(); }; document.addEventListener("keydown", handler, true); return () => document.removeEventListener("keydown", handler, true); - }, [onEscape, enabled]); + }, [enabled]); }Additionally, note that
stopPropagation()prevents the event from reaching child/parent element handlers but does not prevent other listeners on the same node (document) from firing. If twouseEscapeKeyoverlays are ever open simultaneously, a single Escape closes both.stopImmediatePropagation()would be needed for true one-layer-at-a-time semantics. This is low-risk given current usage patterns but worth noting if the comment's "nested pickers close one layer at a time" guarantee is intended to cover that case.🤖 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 `@src/hooks/useEscapeKey.ts` around lines 17 - 28, Stabilize the callback in useEscapeKey by storing onEscape in a ref and updating that ref each render, then remove onEscape from the useEffect dependency array so the document listener is attached only when enabled changes. Preserve the existing Escape handling and cleanup; consider replacing stopPropagation with stopImmediatePropagation if the hook must guarantee only one simultaneously mounted overlay closes.
🤖 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.
Nitpick comments:
In `@mobile/app/sync.tsx`:
- Around line 34-38: Replace the manual useFocusEffect wrapper in the sync
screen with the shared useRefreshOnFocus(refresh) hook, reusing the existing
refresh callback that calls iCloudAvailable().then(setAvailable).catch(() =>
setAvailable(false)).
In `@src/hooks/useEscapeKey.ts`:
- Around line 17-28: Stabilize the callback in useEscapeKey by storing onEscape
in a ref and updating that ref each render, then remove onEscape from the
useEffect dependency array so the document listener is attached only when
enabled changes. Preserve the existing Escape handling and cleanup; consider
replacing stopPropagation with stopImmediatePropagation if the hook must
guarantee only one simultaneously mounted overlay closes.
In `@src/lib/markdown/pipeline.test.ts`:
- Around line 70-74: Add a regression test alongside the existing callout test
that passes raw HTML containing a <mark> element through the markdown pipeline
and asserts the resulting tree retains a mark element, verifying the
KNOWN_HTML_TAGS allowlist behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 3b07b06b-fae1-4ea3-8b09-a742fda1501b
📒 Files selected for processing (44)
changelogs/v2.4.7.mdelectron/embeddings/cosine.tselectron/embeddings/sections.tsmobile/app/(tabs)/calendar/index.tsxmobile/app/(tabs)/chat/index.tsxmobile/app/(tabs)/graph/index.tsxmobile/app/(tabs)/projects/index.tsxmobile/app/(tabs)/search/index.tsxmobile/app/card/new.tsxmobile/app/conflicts.tsxmobile/app/note/new.tsxmobile/app/sync.tsxmobile/changelogs/v0.1.3.mdmobile/src/components/AiSettingsForm.tsxmobile/src/components/EmbeddingsCard.tsxmobile/src/components/NotFound.tsxmobile/src/components/NoteEditorBody.tsxmobile/src/components/ResultRow.tsxmobile/src/components/SectionLabel.tsxmobile/src/components/SourcePicker.tsxmobile/src/components/TaskChips.tsxmobile/src/components/WorkspaceHeaderMenu.tsxmobile/src/notes/embeddings.tsmobile/src/screens/CardDetailScreen.tsxmobile/src/screens/NoteDetailScreen.tsxmobile/src/screens/ProjectCalendarScreen.tsxmobile/src/screens/ProjectScreen.tsxmobile/src/sync/useSyncSources.tsmobile/src/sync/useSyncStatus.tsmobile/src/theme.tsshared/embeddings/vector.test.tsshared/embeddings/vector.tsshared/notes/sections.test.tsshared/notes/sections.tsshared/ui/constants.tssrc/components/notes/BacklinksPanel.tsxsrc/components/notes/markdown-pipeline.bench.test.tssrc/components/notes/note-editor.tsxsrc/components/search/search-panel.tsxsrc/components/ui/date-picker.tsxsrc/hooks/useEscapeKey.tssrc/lib/markdown/pipeline.test.tssrc/lib/markdown/pipeline.tsxtsconfig.electron.json
💤 Files with no reviewable changes (1)
- tsconfig.electron.json
…y hulls Knowledge Graph header controls fit better: - Label density (smart/all/minimal) moves from an in-body pill+overlay to a native Stack.Toolbar placement="left" menu (force mode only), mirroring the Force/Radial menu on the right. labelMode is lifted to the screen and passed to the WebView (controlled, like mode/onModeChange); the in-body labels pill stays as an uncontrolled fallback for symmetry. - The Semantic-links toggle moves out of the right layout menu into an in-body pill immediately left of the Hulls toggle (force mode only, when Apple embeddings are supported). Threaded showSemantic/onToggleSemantic/ semanticAvailable into the WebView. Adds ICON_LABELS (textformat / text_fields) toolbar icon.
- Semantic pill shows an ActivityIndicator while the on-device semantic pass computes (it can take a second or two); the pill is disabled during the load. semanticLoading is tracked in the screen (set around the async loadSemantic, guarded by the same monotonic seq token) and threaded into the WebView. - Match desktop iconography: semantic control uses Network (lucide), same as the desktop KnowledgeGraphView; the label-density menu uses textformat / text_fields (the SF Symbol / Material analog of lucide's Type used for desktop labels).
The semantic pill's spinner never showed because loadSemantic was only invoked by useRefreshOnFocus (focus / data-change) — tapping the pill flipped showSemantic but didn't re-run the load on its own. Drive it from a dedicated useEffect keyed on loadSemantic (which changes with showSemantic), so the spinner appears the instant the pill is tapped; keep useDataChanged for inbound-sync refreshes while it's on. Also give the label-density menu items distinct icons (Smart=sparkles, All=larger-text, Minimal=smaller-text) so the menu reads like the Force/Radial menu (icon + checkmark + label per row) instead of checkmark + text only.
…bView
Root cause of the slow/janky graph toggles: source={{ html }} was memoized
on [filtered, showHulls, labelMode, mode], so toggling semantic links,
hulls, or label density regenerated the whole HTML string and forced a full
WebView document reload — re-injecting the ~274KB D3 bundle and restarting
the force simulation from scratch every time.
Now the HTML is rebuilt only on STRUCTURAL changes (node set / layout mode /
theme, via structuralKey). Cheap force-mode toggles push a patch through a
new window.__update() bridge via injectJavaScript:
- showHulls -> redraw hulls in place
- labelMode -> re-run label visibility at the current zoom
- links -> rebind the link selection, re-seed the link force, and give
the sim a gentle alpha bump (not a full restart) so semantic edges settle
Also: loadSemantic now yields a frame (requestAnimationFrame) after setting
the loading flag so the pill's spinner actually paints before the on-device
embedding pass blocks the JS thread.
Brings the desktop project Overview to mobile as a third segment (Overview | Notes | Board) inside ProjectScreen, defaulting to Overview. - shared/overview/metrics.ts: pure computeProjectMetrics() ported from the desktop useProjectMetrics hook (completion %, done/open split, due-soon within 7d, overdue count, open-task priority counts, pinned/recent notes, activity grouped by day). Platform-agnostic — depends only on shared/format/date and shared/ui/constants — so desktop can adopt it later. +11 vitest tests. - shared/ui/constants.ts: add COLUMN_COLORS + COLUMN_TYPE_ORDER + ColumnType (pure data, previously desktop-only in src/lib/constants). - mobile getProjectOverview(): one focused read returning project meta + columns (with type) + cards (with due_date/updated_at) + notes, so the Overview stays self-contained without widening the hot board/notes queries. - mobile OverviewTab + ProgressRing (react-native-svg): header pills, 3 stat cards, column & priority breakdowns, due-soon list, pinned/recent notes, activity feed. Reuses SectionLabel/TagChips/ProjectIcon/PressableScale/ EmptyState + shared PRIORITY_COLOR/COLUMN_COLORS/date helpers. Deliberately read-only for now: project editing, code-directory picker, and the MCP tools panel are desktop-only concepts (tracked as follow-ups); Chat is its own tab so no pinned chat bar.
Two calendar fixes across desktop and mobile: 1. Cards render in their actual day cell for PAST days too. Desktop's bucketByDate previously kept overdue cards OUT of byDate, so scrolling to a past week showed an empty calendar. byDate now contains every dated card (past included); `overdue` is an additive list that only drives the tray. (Mobile already kept past cards in byDay.) 2. Tasks in a Done column no longer appear in the overdue tray (they still keep their day chip). Desktop: bucketByDate takes an isDone(card) predicate; CalendarView derives the done-column id set from the store. Mobile: listCardsWithDueDates LEFT JOINs board_columns to project an is_done flag, and CalendarView excludes is_done cards from the overdue list. Updated calendar-utils tests (past-in-byDate + done-excluded-from-overdue).
…eduled tray
Centralizes mobile drag-and-drop into one engine and reuses it on the calendar,
matching desktop's reschedule + unscheduled bucket.
Shared drag core (mobile/src/dnd/):
- useDragController<T>: long-press lift, a window-frame drop-zone registry, a
generic rectangle hit-test worklet (tests x AND y, so it works for the board's
column strip and the calendar's 7-wide grid alike), haptics, and scroll-lock.
Returns plain render state (dragging/scrollLocked) + ref-callbacks
(setContainer/registerZone) + reanimated SharedValues + panGesture(item,zone).
- useZoneHighlight: UI-thread hover highlight for a drop zone.
- DragOverlay: the floating clone that follows the finger.
DraggableBoard is refactored onto the core (columns are the drop zones); the
ghost slot, hover highlight and lifted overlay behaviour are preserved.
CalendarView gains drag-to-reschedule: day cells register as drop zones (keyed
by their yyyy-MM-dd), chips are draggable, and a new collapsible Unscheduled
tray (droppable) lists undated tasks — drag onto a day to schedule, drag a dated
task into the tray to clear its due date. Wired into both the workspace calendar
tab and the per-project calendar; commits via updateTask({ dueDate }).
The drop-resolution rules move to shared/calendar/dnd.ts (+tests) so desktop and
mobile reschedule identically; the desktop calendar-dnd module is now a
re-export shim. New listUnscheduledCards() query backs the tray.
The react-hooks/refs lint rule mis-flags every ctrl.* access because the
controller bundles reanimated SharedValues (structurally ref-like but only read
inside worklets) — disabled file-level in the two consumers with rationale.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
shared/overview/metrics.ts (1)
135-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
pinnedNotesandrecentNotesdon't sort byupdatedAtbefore slicing.Both
notes.filter(…).slice(0, N)calls rely on the input being pre-sorted byupdatedAtdescending. If it isn't, "Recent notes" may show stale entries instead of the most recent ones. The function already sortsactivityItemsbyupdatedAt(line 157), so this is an inconsistency.♻️ Proposed refactor: sort before slicing
- const pinnedNotes = notes.filter((n) => n.isPinned).slice(0, 4); - const recentNotes = notes.filter((n) => !n.isPinned).slice(0, 5); + const pinnedNotes = notes + .filter((n) => n.isPinned) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, 4); + const recentNotes = notes + .filter((n) => !n.isPinned) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + .slice(0, 5);🤖 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 `@shared/overview/metrics.ts` around lines 135 - 136, Sort notes by updatedAt descending before slicing in the pinnedNotes and recentNotes calculations, rather than relying on the input order; preserve the existing pinned/non-pinned filtering and limits.
🤖 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 `@mobile/src/dnd/useDragController.ts`:
- Around line 49-50: Update the drag lifecycle in useDragController so scrolling
is not locked for the entire drag: implement edge-triggered auto-scroll while
dragging, remeasure drop zones after scrolling, and stop scrolling when the drag
ends; ensure DraggableBoard and CalendarView can reach off-screen columns and
the Unscheduled tray.
In `@shared/overview/metrics.ts`:
- Around line 120-123: The dueCards date comparisons and sorting use new Date on
bare ISO dates, causing UTC/local-calendar mismatches. Replace these conversions
with parseIsoLocal for both the filter against in7Days and the sort comparator,
matching the date handling used by getDueDateStatus.
---
Nitpick comments:
In `@shared/overview/metrics.ts`:
- Around line 135-136: Sort notes by updatedAt descending before slicing in the
pinnedNotes and recentNotes calculations, rather than relying on the input
order; preserve the existing pinned/non-pinned filtering and limits.
🪄 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: 7822c5cc-fe62-4c11-8373-3676c6b2547b
📒 Files selected for processing (22)
changelogs/v2.4.7.mdmobile/app/(tabs)/calendar/index.tsxmobile/changelogs/v0.1.3.mdmobile/src/components/CalendarView.tsxmobile/src/components/DraggableBoard.tsxmobile/src/components/overview/OverviewTab.tsxmobile/src/components/overview/ProgressRing.tsxmobile/src/db/queries.tsmobile/src/dnd/DragOverlay.tsxmobile/src/dnd/index.tsmobile/src/dnd/useDragController.tsmobile/src/screens/ProjectCalendarScreen.tsxmobile/src/screens/ProjectScreen.tsxshared/calendar/dnd.test.tsshared/calendar/dnd.tsshared/overview/metrics.test.tsshared/overview/metrics.tsshared/ui/constants.tssrc/components/calendar/CalendarView.tsxsrc/components/calendar/calendar-dnd.tssrc/components/calendar/calendar-utils.test.tssrc/components/calendar/calendar-utils.ts
✅ Files skipped from review due to trivial changes (2)
- shared/calendar/dnd.ts
- changelogs/v2.4.7.md
🚧 Files skipped from review as they are similar to previous changes (1)
- shared/ui/constants.ts
…t polish Desktop — workspace-wide Calendar: - New "calendar-all" view in the sidebar just above Knowledge Graph (bottom workspace nav), wired through activeView/lastContentView, ORDERED_VIEWS shortcut, and the page.tsx router (<CalendarView scope="workspace" />). - CalendarView gains a `scope` prop: "project" (existing, activeProjectId) or "workspace" (all non-archived workspace cards, filtered by a dedicated calendarProjectIds store field so it doesn't touch the graph/insights scope). - Extracted the project multi-select into a shared <ProjectScopePicker>, now reused by Insights, Knowledge Graph and the workspace Calendar (dedupes the two inline copies; each got its own click-away scrim). - Unscheduled tray: capped height (max-h-40, scrolls) so a big backlog no longer covers the grid, and grouped into labelled per-project sections in workspace scope (flat list per-project). Mobile: - Per-project calendar now shows Today + Month/Week in the native header toolbar, matching the Calendar tab (drives layout + todayNonce). - Overdue tray chips are draggable (long-press → drop on a day / Unscheduled tray to reschedule or clear); horizontal tray locks scroll while lifting. - Unscheduled tray grouped by project (workspace scope), height-capped, three chips per row; selected/today day use an accent background wash (matches desktop) instead of an outline. - Fix: chat composer image-attachment icon drifted off-centre when the keyboard opened — GlassMenu now takes a containerStyle applied to its outermost (flex child) element, so alignSelf:center reaches the actual flex item, not the inner trigger the parent flexbox can't see.
… timezone-safe dates Drag auto-scroll (shared drag core): - While dragging, holding near a scroll edge now auto-scrolls the list so off-screen drop zones (board columns, calendar days, the Unscheduled tray) stay reachable. Runs on the UI thread via scrollTo; drop-zone hit-testing is offset-corrected for the live scroll position so targets stay accurate mid scroll (no re-measure lag). - maxScroll is seeded from the ScrollView's onLayout + onContentSizeChange, so auto-scroll works on the first drag instead of only after a manual scroll (maxScroll previously came only from onScroll). Board + calendar wire an Animated.ScrollView with scrollRef/scrollHandler + scrollAxis (x / y). - Pure edge-ramp + frame-offset math extracted to src/dnd/autoScroll.ts with unit tests (14); the worklet path inlines the same math to avoid fragile cross-module worklet calls under reanimated v4. Crash fix: - useZoneHighlight and DragOverlay now capture the individual SharedValues instead of the whole controller object, so worklets no longer try to copy the non-serialisable scroll handler (WorkletEventHandlerNative) to the UI thread (was crashing on opening the Calendar). Calendar drag polish: - The lifted chip clone is a fixed width regardless of source zone (day cell vs full-width Unscheduled tray no longer produce different-sized clones). - The chip lifts clear above the finger with a small downward pointer tail at the drop point, so it stays readable and unobscured. Overview metrics (shared): - dueCards now parse bare yyyy-MM-dd dates as local calendar days (parseIsoLocal) for both the 7-day-window filter and the sort, matching getDueDateStatus, so "due soon" doesn't drift by a day in negative-offset timezones. - pinned/recent notes are sorted by updatedAt desc before slicing, so the lists are correct regardless of caller input order. +2 tests.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@src/components/shared/ProjectScopePicker.tsx`:
- Around line 40-47: Add type="button" to every dropdown button in
ProjectScopePicker, including the “All projects” button, the per-project toggle
button, and the button shown in the diff, without changing their click behavior.
🪄 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: dbfdbd35-3820-440d-bb2b-20c2d4514339
📒 Files selected for processing (22)
mobile/app/(tabs)/chat/index.tsxmobile/changelogs/v0.1.3.mdmobile/src/components/CalendarView.tsxmobile/src/components/DraggableBoard.tsxmobile/src/components/GlassMenu.tsxmobile/src/dnd/DragOverlay.tsxmobile/src/dnd/autoScroll.test.tsmobile/src/dnd/autoScroll.tsmobile/src/dnd/index.tsmobile/src/dnd/useDragController.tsmobile/src/screens/ProjectCalendarScreen.tsxshared/overview/metrics.test.tsshared/overview/metrics.tssrc/app/page.tsxsrc/components/calendar/CalendarView.tsxsrc/components/calendar/UnscheduledTray.tsxsrc/components/graph/KnowledgeGraphView.tsxsrc/components/insights/InsightsView.tsxsrc/components/layout/sidebar.tsxsrc/components/shared/ProjectScopePicker.tsxsrc/store/slices/ui.tssrc/types/index.ts
✅ Files skipped from review due to trivial changes (1)
- mobile/changelogs/v0.1.3.md
🚧 Files skipped from review as they are similar to previous changes (8)
- mobile/src/dnd/index.ts
- mobile/src/dnd/DragOverlay.tsx
- shared/overview/metrics.test.ts
- shared/overview/metrics.ts
- mobile/src/screens/ProjectCalendarScreen.tsx
- mobile/src/components/CalendarView.tsx
- mobile/src/components/DraggableBoard.tsx
- mobile/src/dnd/useDragController.ts
…too)
Brings mobile chat navigation to desktop parity. Previously mobile only linked
notes, by title, resolved at tap time (lower(title)=? LIMIT 1, no scope),
pushing /note/[id] and silently no-oping on a miss — so taps could open the
wrong note (title collisions), do nothing (non-exact title / cross-source), and
cards were never reachable from chat at all.
Shared (shared/notes/markdown.ts):
- preprocessCairnMarkdown(src, resolve?) gains an optional WikilinkResolver so
callers with DB context (chat) resolve [[Title]] -> concrete note/card id and
bake cairn://note/<id> or cairn://task/<id> into the URL at preprocess time
(desktop pattern). Without the hook, note bodies keep the cheap title-encoded
form resolved on tap.
- Add CAIRN_CARD_SCHEME + cardIdFromUrl(). +14 unit tests.
Mobile:
- MarkdownView gains a `resolveLinks` prop; when on it builds a DB resolver
(findNoteIdByTitle -> note, else findCardIdByTitle -> card) and routes
cairn://task/ -> /card/[id], cairn://note/ -> /note/[id] (value treated as a
title first, else as a baked id). Chat bubbles render with resolveLinks.
- New findCardIdByTitle query.
- Chat tool chips carry an optional ref {kind,id} derived from each tool's
name + args/result (ensure_note/get_note/append_to_note/patch_note/create_note
-> note; create_task -> card; errored tools excluded). Chips with a ref render
as a Pressable that opens the item by id. Persisted via chat_local.
type-check:all + mobile type-check + eslint clean; shared vitest 122 (incl. 14
new). Changelog mobile v0.1.3.
What does this PR do?
Batch of cleanup + fixes on top of
main(824234d): fixes two desktop bugs (wikilinks rendering as literal text; note-preview re-render perf), makes Escape close overlays regardless of focus, extracts byte-identical embeddings logic intoshared/so desktop and mobile can't drift, and consolidates repeated mobile UI into reusable components/hooks. No user-facing behaviour change except the noted desktop fixes.Type of change
Screenshots / recording
Checklist
npm run type-check:allpassesnpm run lintpassesnpm testpasses (runsnpm run compilefirst soelectron/bundle-guard.test.tsactually executes —npm run test:bundleto run just that)npm run test:e2epasses (run before merging UI changes or cutting a release)var(--accent),var(--text-primary), etc.)text-[Npx]pixel font classes — rem equivalents only (text-[0.714rem],text-xs, etc.)handle()and returnIpcResult<T>— N/A (no new IPC)schema.ts— N/A (no schema change)electron/db/queries.ts— N/A (no new SQL)dependenciesordevDependencies— N/A (none added)--external:<pkg>flag — N/A (compile script unchanged)Notes for reviewer
Highlights by commit:
477d08e— wikilink regression (rehypeEscapeUnknownTagsallow-list omittedwikilink) + preview perf (ReactMarkdowncomponentsmap depended on the wholenotesarray/note.content, forcing full re-parse + KaTeX re-mount on any store update; now ref-based). Corrected the markdown bench (it omittedrehypeRaw+ the escaper) and added stage benches + a regression test.d291212— newuseEscapeKeyhook (document-level, capture-phase); adopted in SearchPanel, DatePicker (had no Escape at all), and the note tag picker.62afa78—shared/notes/sections.ts+shared/embeddings/vector.ts; desktop keeps thin re-export shims; test suites moved toshared/. Note: droppedrootDirfromtsconfig.electron.json— it only affected the--noEmittype-check (esbuild does the real build), letting electron import top-levelshared/the same wayelectron/sync/already does.2680316,8f2ae6a,adf90fa— mobile: icon-only workspace switcher + removed redundant Sync-page picker; extractedtype.overline/SectionLabel,useRefreshOnFocus,useSyncSources,PriorityChips/ColumnChips(+ sharedPRIORITIES),ResultRow,NoteEditorBody,NotFound.Changelogs updated:
changelogs/v2.4.7.md(desktop),mobile/changelogs/v0.1.3.md(mobile). I rantype-check:all+ mobiletype-check/eslint+vitest run sharedand the desktop markdown/search/notes suites (all green); the two unchecked boxes (npm testwith compile,test:e2e) I didn't run locally.Summary by CodeRabbit