Skip to content

feat(ui): render markdown bold/italic in page title, search results, and snippets#108

Merged
tstapler merged 5 commits into
mainfrom
stelekit-markdown-headers
May 29, 2026
Merged

feat(ui): render markdown bold/italic in page title, search results, and snippets#108
tstapler merged 5 commits into
mainfrom
stelekit-markdown-headers

Conversation

@tstapler
Copy link
Copy Markdown
Owner

@tstapler tstapler commented May 29, 2026

Summary

Markdown rendering (primary change):

  • Fix page title (PageView.kt) to render bold/italic markdown via parseMarkdownWithStyling instead of plain Text(page.name)
  • Fix search result titles (SearchDialog.kt) to render markdown inline formatting
  • Fix search snippets (SnippetText.kt) to compose FTS5 <em> highlights with markdown bold/italic via a new parseEmTags + remapRanges helper pipeline
  • Add 10 new unit tests: 7 span-style tests in ParseMarkdownWithStylingTest + 3 parseEmTags tests in SnippetTextTest

Journal / file-sync (pre-existing on branch, carried into this PR):

  • JournalService: inject Clock for testable timestamp/date resolution
  • StelekitViewModel: start midnight boundary watcher to ensure a journal page exists each day
  • FileRegistry: invalidate shadow cache before reading externally-changed files
  • ShadowFileCache (Android): add safe shadow-file path resolution

No parser changes — all markdown rendering gaps were in the Compose UI layer.

Test plan

  • ./gradlew jvmTest — BUILD SUCCESSFUL
  • ParseMarkdownWithStylingTest — 7 new tests: bold, italic, bold-italic, underscore variants, marker stripping, plain text
  • SnippetTextTest — 3 new tests: single <em> range, multiple ranges, no tags
  • JournalServiceTest — clock injection coverage
  • FileRegistryTest — stale-shadow regression tests
  • GraphLoaderProgressiveTest — midnight watcher behavior tests
  • Screenshot reference images regenerated locally via ./gradlew recordRoborazziJvm

🤖 Generated with Claude Code

tstapler and others added 4 commits May 29, 2026 13:28
… journal

Two bugs in file watching and journal lifecycle:

1. FileRegistry.detectChanges was calling fileSystem.readFile after a mtime
   change without first invalidating the shadow cache. On Android, readFile
   hits the shadow first — stale content matched the stored hash, so the
   change was silently dropped as an own-write.

   Fix: call fileSystem.invalidateShadow before readFile in the
   modTime > lastKnown branch. No-op on JVM; zero cost on desktop.

2. ensureTodayJournal was only called once at startup (onPhase1Complete).
   When the app was open across midnight, the new day's journal was never
   created.

   Fix: launch a midnight-boundary watcher coroutine on the graph scope
   that computes delay to next local midnight, sleeps, then calls
   ensureTodayJournal. Stores lastJournalDate so redundant calls are
   skipped (also guards against clock-precision double-fires). Logs
   seconds until next check so the timing is observable.

Also injects Clock into JournalService for testability, and adds:
- FileRegistryTest: stale-shadow regression test
- GraphLoaderProgressiveTest: midnight-boundary + guard tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…er tests

Fix DurationUnit compile error in JournalServiceTest (Duration.Companion.hours
does not exist; use 1.toDuration(DurationUnit.HOURS)).

Add:
- JournalServiceTest: clock injection test seeding fixedDate via currentSystemDefault + 1h
- FileRegistryTest: shadow stale-read regression + call-order assertion
- GraphLoaderProgressiveTest: millisUntilNextMidnight and midnight-watcher tests
  now call the actual production method on StelekitViewModel instead of reimplementing
  the formula inline
- FakeClock utility in jvmTest testing package

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…asmJS

kotlin.concurrent.Volatile is unavailable in the WasmJS target. The field is
only accessed within the single midnightWatcherJob coroutine so no
synchronization is needed; removing the annotation fixes the Wasm/JS Compile CI check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and snippets

PageView, SearchDialog, and SnippetText were passing raw markdown strings to
plain Text() composables. Switch all three to parseMarkdownWithStyling() so
**bold** and *italic* syntax renders as styled text instead of raw asterisks.
Also extracts a parseEmTags() helper in SnippetText to compose FTS5 highlights
with markdown spans, and adds 10 unit tests covering bold/italic span output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 29, 2026 22:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds Compose UI support for rendering inline markdown styling in page/search titles and snippets, while also introducing additional journal midnight-watcher, clock-injection, and Android shadow-cache/file-registry changes beyond the described markdown scope.

Changes:

  • Renders markdown bold/italic in page titles, search result titles, and snippets.
  • Adds snippet and markdown styling tests.
  • Adds journal clock/midnight watcher behavior plus file-registry/shadow-cache regression coverage.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/screens/PageView.kt Renders page title via parseMarkdownWithStyling.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SearchDialog.kt Renders search result titles as annotated markdown text.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SnippetText.kt Adds <em> parsing and composes snippets with markdown styling.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/ui/components/SnippetTextTest.kt Adds tests for <em> stripping/range extraction.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/ui/components/ParseMarkdownWithStylingTest.kt Adds bold/italic span tests.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/JournalService.kt Injects Clock for journal timestamps/date resolution.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/repository/JournalServiceTest.kt Verifies injected clock usage.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/StelekitViewModel.kt Starts a midnight watcher to ensure daily journals.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/testing/FakeClock.kt Adds a mutable test clock helper.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/db/GraphLoaderProgressiveTest.kt Adds midnight watcher-related tests.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/FileRegistry.kt Invalidates shadow cache before reading changed files.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/db/FileRegistryTest.kt Adds stale-shadow regression tests.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/ShadowFileCache.kt Adds safe shadow-file path resolution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SearchDialog.kt Outdated
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

JVM Load Benchmark (Desktop)

Synthetic in-memory benchmark measuring load performance for the desktop (JVM) app.
Comparing 50dca5c (this PR) vs acc5b5b (baseline)
Graph config: xlarge — 230 pages

Metric This PR Baseline Delta
Phase 1 TTI ↓ 9ms 10ms -1ms (-10%) ✅
Phase 2 background ↓ 3ms 3ms 0 (0%)
Phase 3 index ↓ 14ms 14ms 0 (0%)
Total ↓ 25ms 26ms -1ms (-4%) ✅
Write p95 (baseline) ↓ 34ms 29ms +5ms (+17%) ⚠️
Write p95 (under load) ↓ n/a n/a
Jank factor ↓ n/a n/a
↓ lower is better
Flamegraphs (this PR) **Allocation** — object allocation pressure (JDBC/SQLite churn)

Alloc flamegraph not available

CPU — method-level hotspots by on-CPU time

CPU flamegraph not available

Top SQL queries by total time (this PR) | table:operation | calls | p50 | p99 | max | total | |-----------------|-------|-----|-----|-----|-------| | `pages:select` | 2 | 1ms | 1ms | 1ms | 1ms |
Top allocation hotspots (this PR) `70.3%` byte[]_[k] `4.2%` java.lang.String_[k] `3.1%` java.lang.Object[]_[k] `2.1%` java.lang.StringBuilder_[k] `1.6%` java.lang.Class_[k]
Top CPU hotspots (this PR) `99.4%` /usr/lib/x86_64-linux-gnu/libc.so.6 `0.1%` /tmp/sqlite-3.51.3.0-f2ea01e1-cae7-4130-8341-401d0e37ce3c-libsqlitejdbc.so `0.1%` SR_handler `0%` __libc_pwrite `0%` kotlinx/coroutines/JobSupport.completeStateFinalization_[0]

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Android Load Benchmark

Instrumented benchmark on an API 30 x86_64 emulator — 500-page synthetic graph.

Comparing 50dca5c (this PR) vs acc5b5b (baseline)
Device: API 30 x86_64 emulator — 530 pages loaded

Graph Load

Metric This PR Baseline Delta
Phase 1 TTI ↓ 86ms 90ms -4ms (-4%) ✅
Phase 3 index ↓ 4588ms 4765ms -177ms (-4%) ✅

Interactive Write Latency (during Phase 3)

Metric This PR Baseline Delta
Write p95 (baseline) ↓ 9ms 6ms +3ms (+50%) ⚠️
Write p95 (during phase 3) ↓ 10ms 11ms -1ms (-9%) ✅
Jank factor ↓ 1.11x 1.83x -0.72x (-39%) ✅
Concurrent writes ↑ 23 22 +1ms (+5%) ✅

SAF I/O Overhead (ContentProvider vs direct File read)

Measures Binder IPC cost added by ContentResolver per readFile() call.
Real SAF via ExternalStorageProvider will be higher on device; this is a lower bound.

Metric This PR Baseline Delta
Direct read / file ↓ 0.0ms 0.0ms 0 (0%)
Provider read / file ↓ 0.1ms 0.2ms 0ms (-24%) ✅
IPC overhead ratio ↓ 4x 5x -1x (-20%) ✅
↓ lower is better · ↑ higher is better

…elected fontWeight

- SnippetText: add remapRanges() to rebase <em> highlight offsets onto post-markdown
  text coords, preventing IndexOutOfBounds when markdown markers precede <em> spans
- SearchDialog: restore FontWeight.Medium for selected rows lost in BasicText migration
@tstapler tstapler merged commit aa7e5a5 into main May 29, 2026
12 checks passed
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.

2 participants