feat(ui): render markdown bold/italic in page title, search results, and snippets#108
Conversation
… 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>
There was a problem hiding this comment.
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.
JVM Load Benchmark (Desktop)Synthetic in-memory benchmark measuring load performance for the desktop (JVM) app.
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] |
Android Load BenchmarkInstrumented benchmark on an API 30 x86_64 emulator — 500-page synthetic graph. Comparing Graph Load
Interactive Write Latency (during Phase 3)
SAF I/O Overhead (ContentProvider vs direct File read)Measures Binder IPC cost added by ContentResolver per readFile() call.
|
…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
Summary
Markdown rendering (primary change):
PageView.kt) to render bold/italic markdown viaparseMarkdownWithStylinginstead of plainText(page.name)SearchDialog.kt) to render markdown inline formattingSnippetText.kt) to compose FTS5<em>highlights with markdown bold/italic via a newparseEmTags+remapRangeshelper pipelineParseMarkdownWithStylingTest+ 3parseEmTagstests inSnippetTextTestJournal / file-sync (pre-existing on branch, carried into this PR):
JournalService: injectClockfor testable timestamp/date resolutionStelekitViewModel: start midnight boundary watcher to ensure a journal page exists each dayFileRegistry: invalidate shadow cache before reading externally-changed filesShadowFileCache(Android): add safe shadow-file path resolutionNo parser changes — all markdown rendering gaps were in the Compose UI layer.
Test plan
./gradlew jvmTest— BUILD SUCCESSFULParseMarkdownWithStylingTest— 7 new tests: bold, italic, bold-italic, underscore variants, marker stripping, plain textSnippetTextTest— 3 new tests: single<em>range, multiple ranges, no tagsJournalServiceTest— clock injection coverageFileRegistryTest— stale-shadow regression testsGraphLoaderProgressiveTest— midnight watcher behavior tests./gradlew recordRoborazziJvm🤖 Generated with Claude Code