Skip to content

Shared-logic extraction, mobile component reuse, and desktop bug fixes - #81

Merged
ddutchie merged 17 commits into
mainfrom
ddutchie/sharedcleanup
Jul 10, 2026
Merged

ddutchie merged 17 commits into
mainfrom
ddutchie/sharedcleanup

Conversation

@ddutchie

@ddutchie ddutchie commented Jul 10, 2026 •

Copy link
Copy Markdown
Owner

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 into shared/ 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

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

Screenshots / recording

Checklist

  • npm run type-check:all passes
  • npm run lint passes
  • npm test passes (runs npm run compile first so electron/bundle-guard.test.ts actually executes — npm run test:bundle to run just that)
  • npm run test:e2e passes (run before merging UI changes or cutting a release)
  • No hardcoded colours — CSS variables only (var(--accent), var(--text-primary), etc.)
  • No text-[Npx] pixel font classes — rem equivalents only (text-[0.714rem], text-xs, etc.)
  • New IPC handlers wrapped in handle() and return IpcResult<T> — N/A (no new IPC)
  • New DB migrations appended (not edited) in schema.ts — N/A (no schema change)
  • New SQL goes in electron/db/queries.ts — N/A (no new SQL)
  • New dependencies or devDependencies — N/A (none added)
  • New MCP tools registered — N/A (no new tools)
  • If you added/removed a --external:<pkg> flag — N/A (compile script unchanged)

Notes for reviewer

Highlights by commit:

  • 477d08e — wikilink regression (rehypeEscapeUnknownTags allow-list omitted wikilink) + preview perf (ReactMarkdown components map depended on the whole notes array/note.content, forcing full re-parse + KaTeX re-mount on any store update; now ref-based). Corrected the markdown bench (it omitted rehypeRaw + the escaper) and added stage benches + a regression test.
  • d291212 — new useEscapeKey hook (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 to shared/. Note: dropped rootDir from tsconfig.electron.json — it only affected the --noEmit type-check (esbuild does the real build), letting electron import top-level shared/ the same way electron/sync/ already does.
  • 2680316, 8f2ae6a, adf90fa — mobile: icon-only workspace switcher + removed redundant Sync-page picker; extracted type.overline/SectionLabel, useRefreshOnFocus, useSyncSources, PriorityChips/ColumnChips (+ shared PRIORITIES), ResultRow, NoteEditorBody, NotFound.

Changelogs updated: changelogs/v2.4.7.md (desktop), mobile/changelogs/v0.1.3.md (mobile). I ran type-check:all + mobile type-check/eslint + vitest run shared and the desktop markdown/search/notes suites (all green); the two unchecked boxes (npm test with compile, test:e2e) I didn't run locally.

Summary by CodeRabbit

  • New Features
    • Mobile: added a Project Overview tab; unified search results with shared row styling; reusable priority/column chips; shared section labels; updated note editing via a shared editor body; task/note tool chips are tappable in chat.
    • Calendar: added an Unscheduled tray and drag-to-reschedule due dates, including a workspace-level “Calendar” scope; Knowledge Graph now supports label-density controls plus a semantic-links toggle.
  • Bug Fixes
    • Calendar: overdue tasks now appear in both the overdue tray and their day cells; completed past-due tasks no longer show as overdue.
    • Notes/search: wikilinks/callouts render correctly and previews are more responsive; Escape consistently closes the search panel and open pickers/popovers regardless of cursor focus.

ddutchie added 6 commits July 9, 2026 22:16
…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.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 85fc0b97-0ed5-4f71-a38c-6395acff50ea

📥 Commits

Reviewing files that changed from the base of the PR and between 4920edb and ddd71e9.

📒 Files selected for processing (8)
  • mobile/app/(tabs)/chat/index.tsx
  • mobile/changelogs/v0.1.3.md
  • mobile/src/components/MarkdownView.tsx
  • mobile/src/db/chat-store.ts
  • mobile/src/db/queries.ts
  • shared/notes/markdown.test.ts
  • shared/notes/markdown.ts
  • src/components/shared/ProjectScopePicker.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/shared/ProjectScopePicker.tsx
  • mobile/src/db/queries.ts

📝 Walkthrough

Walkthrough

This 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.

Changes

Cross-platform behavior and UI consolidation

Layer / File(s) Summary
Shared indexing and mobile foundations
shared/embeddings/*, shared/notes/sections.*, mobile/src/sync/*, mobile/src/components/*, mobile/src/theme.ts
Shared vector, section, priority, refresh, source-discovery, and reusable UI foundations are added or adopted across desktop and mobile.
Calendar and project overview
shared/calendar/*, mobile/src/components/CalendarView.tsx, mobile/src/dnd/*, shared/overview/*, mobile/src/components/overview/*
Calendar overdue handling, shared drop resolution, mobile drag interactions, overview metrics, and the mobile Overview tab are implemented.
Graph controls and workspace navigation
mobile/app/(tabs)/graph/index.tsx, mobile/src/components/KnowledgeGraphWebView.tsx, src/components/shared/ProjectScopePicker.tsx, src/components/calendar/*, src/store/slices/ui.ts
Graph controls use incremental WebView updates, and desktop graph, insights, and calendar views gain shared project-scope selection.
Markdown navigation and Escape handling
shared/notes/markdown.ts, mobile/src/components/MarkdownView.tsx, src/lib/markdown/*, src/hooks/useEscapeKey.ts, src/components/notes/*
Wikilinks resolve to notes or cards, custom markdown elements survive processing, note preview components remain stable, and Escape closes search and picker overlays.
Release notes and configuration
changelogs/*, tsconfig.electron.json
Release notes record the new behavior and the Electron root directory override is removed.

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
Loading

Possibly related PRs

  • ddutchie/cairn#76 — Both PRs modify the shared markdown and wikilink pipeline.
  • ddutchie/cairn#77 — Both PRs modify mobile note embedding section splitting and vector similarity.
  • ddutchie/cairn#78 — Both PRs modify mobile chat tool and reasoning UI.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main themes of the PR: shared logic extraction, mobile component reuse, and desktop bug fixes.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ddutchie/sharedcleanup

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

mobile/app/(tabs)/chat/index.tsx

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

mobile/src/components/MarkdownView.tsx

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

mobile/src/db/chat-store.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 1 others

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.

🧹 Nitpick comments (3)
mobile/app/sync.tsx (1)

34-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider reusing the new shared refresh hook here too.

This screen still wires iCloud availability refresh manually via useFocusEffect(useCallback(() => refresh(), [refresh])), while conflicts.tsx in the same PR was migrated to the shared useRefreshOnFocus hook. For consistency (and to also pick up on sync-data-changed events, not just focus), consider switching this to useRefreshOnFocus(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 value

Consider adding a regression test for raw HTML <mark> tags.

mark was added to KNOWN_HTML_TAGS to allow raw HTML <mark> tags to survive the escape pass (the ==highlight== marks created by rehypeMergedPass run after the escape pass, so they don't need the allowlist entry). A test would guard against mark being 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 win

Consider stabilizing onEscape with a ref to avoid listener churn.

onEscape is in the useEffect dependency array. Two of three consumers (date-picker.tsx line 45 and BacklinksPanel.tsx line 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 two useEscapeKey overlays 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

📥 Commits

Reviewing files that changed from the base of the PR and between 824234d and d291212.

📒 Files selected for processing (44)
  • changelogs/v2.4.7.md
  • electron/embeddings/cosine.ts
  • electron/embeddings/sections.ts
  • mobile/app/(tabs)/calendar/index.tsx
  • mobile/app/(tabs)/chat/index.tsx
  • mobile/app/(tabs)/graph/index.tsx
  • mobile/app/(tabs)/projects/index.tsx
  • mobile/app/(tabs)/search/index.tsx
  • mobile/app/card/new.tsx
  • mobile/app/conflicts.tsx
  • mobile/app/note/new.tsx
  • mobile/app/sync.tsx
  • mobile/changelogs/v0.1.3.md
  • mobile/src/components/AiSettingsForm.tsx
  • mobile/src/components/EmbeddingsCard.tsx
  • mobile/src/components/NotFound.tsx
  • mobile/src/components/NoteEditorBody.tsx
  • mobile/src/components/ResultRow.tsx
  • mobile/src/components/SectionLabel.tsx
  • mobile/src/components/SourcePicker.tsx
  • mobile/src/components/TaskChips.tsx
  • mobile/src/components/WorkspaceHeaderMenu.tsx
  • mobile/src/notes/embeddings.ts
  • mobile/src/screens/CardDetailScreen.tsx
  • mobile/src/screens/NoteDetailScreen.tsx
  • mobile/src/screens/ProjectCalendarScreen.tsx
  • mobile/src/screens/ProjectScreen.tsx
  • mobile/src/sync/useSyncSources.ts
  • mobile/src/sync/useSyncStatus.ts
  • mobile/src/theme.ts
  • shared/embeddings/vector.test.ts
  • shared/embeddings/vector.ts
  • shared/notes/sections.test.ts
  • shared/notes/sections.ts
  • shared/ui/constants.ts
  • src/components/notes/BacklinksPanel.tsx
  • src/components/notes/markdown-pipeline.bench.test.ts
  • src/components/notes/note-editor.tsx
  • src/components/search/search-panel.tsx
  • src/components/ui/date-picker.tsx
  • src/hooks/useEscapeKey.ts
  • src/lib/markdown/pipeline.test.ts
  • src/lib/markdown/pipeline.tsx
  • tsconfig.electron.json
💤 Files with no reviewable changes (1)
  • tsconfig.electron.json

ddutchie added 7 commits July 10, 2026 09:56
…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.

@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: 2

🧹 Nitpick comments (1)
shared/overview/metrics.ts (1)

135-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

pinnedNotes and recentNotes don't sort by updatedAt before slicing.

Both notes.filter(…).slice(0, N) calls rely on the input being pre-sorted by updatedAt descending. If it isn't, "Recent notes" may show stale entries instead of the most recent ones. The function already sorts activityItems by updatedAt (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

📥 Commits

Reviewing files that changed from the base of the PR and between 7c631e2 and ca893fb.

📒 Files selected for processing (22)
  • changelogs/v2.4.7.md
  • mobile/app/(tabs)/calendar/index.tsx
  • mobile/changelogs/v0.1.3.md
  • mobile/src/components/CalendarView.tsx
  • mobile/src/components/DraggableBoard.tsx
  • mobile/src/components/overview/OverviewTab.tsx
  • mobile/src/components/overview/ProgressRing.tsx
  • mobile/src/db/queries.ts
  • mobile/src/dnd/DragOverlay.tsx
  • mobile/src/dnd/index.ts
  • mobile/src/dnd/useDragController.ts
  • mobile/src/screens/ProjectCalendarScreen.tsx
  • mobile/src/screens/ProjectScreen.tsx
  • shared/calendar/dnd.test.ts
  • shared/calendar/dnd.ts
  • shared/overview/metrics.test.ts
  • shared/overview/metrics.ts
  • shared/ui/constants.ts
  • src/components/calendar/CalendarView.tsx
  • src/components/calendar/calendar-dnd.ts
  • src/components/calendar/calendar-utils.test.ts
  • src/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

Comment thread mobile/src/dnd/useDragController.ts
Comment thread shared/overview/metrics.ts Outdated
ddutchie added 2 commits July 10, 2026 12:34
…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.

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between ca893fb and 4920edb.

📒 Files selected for processing (22)
  • mobile/app/(tabs)/chat/index.tsx
  • mobile/changelogs/v0.1.3.md
  • mobile/src/components/CalendarView.tsx
  • mobile/src/components/DraggableBoard.tsx
  • mobile/src/components/GlassMenu.tsx
  • mobile/src/dnd/DragOverlay.tsx
  • mobile/src/dnd/autoScroll.test.ts
  • mobile/src/dnd/autoScroll.ts
  • mobile/src/dnd/index.ts
  • mobile/src/dnd/useDragController.ts
  • mobile/src/screens/ProjectCalendarScreen.tsx
  • shared/overview/metrics.test.ts
  • shared/overview/metrics.ts
  • src/app/page.tsx
  • src/components/calendar/CalendarView.tsx
  • src/components/calendar/UnscheduledTray.tsx
  • src/components/graph/KnowledgeGraphView.tsx
  • src/components/insights/InsightsView.tsx
  • src/components/layout/sidebar.tsx
  • src/components/shared/ProjectScopePicker.tsx
  • src/store/slices/ui.ts
  • src/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

Comment thread src/components/shared/ProjectScopePicker.tsx
ddutchie added 2 commits July 10, 2026 14:03
…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.
@ddutchie
ddutchie merged commit fa04701 into main Jul 10, 2026
6 checks passed
@ddutchie
ddutchie deleted the ddutchie/sharedcleanup branch July 10, 2026 19:27
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