chore(agent): stage memories (#4569)

This commit is contained in:
Huang Xin
2026-06-13 14:57:21 +08:00
committed by GitHub
parent a56cc6c61a
commit b6937f43f1
8 changed files with 246 additions and 0 deletions
@@ -17,6 +17,7 @@
- [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — white↔black flash on swipe+animation only; `#background` was static screen-space and didn't track content during drag/snap; fix = sliding per-view full-bleed segments (`computeBackgroundSegments`) rebuilt on scroll + per-rAF synced to the view transform during snap
- [Duokan fullscreen cover hidden in scroll mode](duokan-fullscreen-cover-scroll.md) — #4379 `data-duokan-page-fullscreen` cover pinned `position:absolute height:100%` collapses against auto-height scroll container; gate fullscreen on `this.#column` + reset stale absolute props on toggle (`setImageSize` in paginator.js)
- [Paginated texture occlusion](paginated-texture-occlusion-4399.md) — #4399 host `.foliate-viewer::before` texture absent in paginated (shown in scrolled); opaque `#background` container (`= fallbackBg`) from the swipe-flash fix occludes it; shared `textureAwareBackground` helper + `hasTexture ? '' : fallbackBg` container
- [Dark-mode texture occluded by body bg (#4446)](dark-mode-texture-body-bg-4446.md) — RESOLVED (PR #4564): `body.theme-dark{bg !important}` from #4392 (v0.11.4, NOT foliate-js) painted iframe bodies opaque dark → occluded host texture + poisoned `docBackground` capture → opaque segments/view bgs; fix = `transparent !important` UNCONDITIONALLY (texture-gating would go stale: capture is once-per-section-load); CDP gotchas = patch ALL multiview iframes, stale preload views survive navigation ±2, load-listener sees exact capture-time state
- [Background overflows column (#4394, PR #4429)](paginator-gutter-bleed-asymmetry-4394.md) — paginated page bg stretched into the outer `--_outer-min` gutter → mixed cover/title 2-up spread shifted off-centre (~250px at 1920px). KEEP the grid (`--_outer-min` keeps margins symmetric); fix = clamp `computeBackgroundSegments` to `[containerStart,containerEnd]` (Math.max/Math.min) so bg stays in its column. 2 wrong tries first (bleed-gating, "page shouldn't be yellow"); foliate submodule needs dev-server RESTART to pick up edits
## Critical Files (Most Bug-Prone)
@@ -45,8 +46,10 @@
- [Android NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) — why NativeFile is slow (4-IPC/chunk + bridge serialization, tauri#9190); RemoteFile CANNOT replace it on Android (asset-protocol Range broken: start>0 → "Failed to fetch", start-0 capped at 1,024,000; plain no-Range fetch returns full file at 281 MB/s); measured 44/100/281 MB/s; speedups = handle-reuse (2.3×), whole-file asset loader (6.3×), or fix wry upstream. Verified live via CDP.
- [Window-state sanitizer (#4398)](window-state-sanitize-4398.md) — Windows launch crash (WebView2 0x80070057) from invalid `.window-state.json` (`-32000` minimized sentinel / `0×0`); our plugin already has upstream #253 fix so bad files are stale; defense-in-depth `window-state-sanitizer` plugin registered BEFORE window-state (plugin init = registration order); coord threshold `-16000` (~halfway to the -32000 sentinel; real desktops sit a few thousand px off origin) keeps multi-monitor negatives
- [Android Open-with intent flow (#4521)](android-open-with-intent-flow.md) — "Open with"/"Send to" pipeline: `NativeBridgePlugin.kt::handleIntent``shared-intent``useAppUrlIngress``useOpenWithBooks` (VIEW=transient→reader, SEND=library+upload). Telegram fails where file-manager works on TWO axes: cold-start delivery (fixed by #4527, on dev NOT released v0.11.4) + foreign-private-file read (Telegram FileProvider non-persistable grant vs shared-storage FUSE real-path). adb MediaStore VIEW repro tests pipeline but CANNOT reproduce the read axis (MANAGE_EXTERNAL_STORAGE bypasses grant)
- [Dict lookup → OEM browser hijack (#4559)](dict-lookup-browser-hijack-4559.md) — VIVO system-dict lookup opened the browser not Eudic. PRIMARY: no `<queries>` for `ACTION_PROCESS_TEXT` under targetSdk36 → dictionary apps invisible, only auto-visible browser returned (fix = add `<queries>` to plugin manifest). SECONDARY: browser registers PROCESS_TEXT + is default → filter browsers in pure `decideLookupDispatch` (explicit/chooser/unavailable). Remember-the-pick via `IntentSender`+`EXTRA_CHOSEN_COMPONENT``LookupChoiceReceiver`→SharedPreferences (`ACTION_CHOOSER` has no native Always); reset row in `CustomDictionaries.tsx`
## Feature Notes
- [Edge TTS word highlighting (#4017, PR #4566)](edge-tts-word-highlighting-4017.md) — keep sentence marks, add word highlight via `audio.metadata` WordBoundary (verbatim input span, 100-ns ticks) synced to `audio.currentTime` by rAF; readaloud endpoint gates on UA (Edg, non-headless) NOT Origin; fixed browser `new WebSocket(url,{headers})` SyntaxError (wss never worked on web); overlay = `<path>` in FOLIATE-PAGINATOR shadow root; dev-web verify recipe (browse --proxy + UA spoof, never Origin header)
- [Reference Pages (#672+#4542, PR #4549)](reference-pages-672-4542.md) — 'reference' progressStyle from foliate `pageItem`/`book.pageList` (numeric-max total rule, roman-tail safe); per-book `referencePageCount` via skipGlobal save; verification EPUBs + dev-web synthetic drag-drop import trick; locale-tail rebase-conflict recipe (checkout --ours → re-extract → re-translate)
- [OPDS Firefox strict-XML parse (#4479)](opds-firefox-strict-xml-4479.md) — MEK feed has junk after `</feed>`; Firefox DOMParser → `<parsererror>` (silent back-nav), Chrome lenient; `parseOPDSXML` slices root start→last close tag; jsdom mirrors Firefox; wired into page.tsx + validateOPDSURL + feedChecker (latter also #4181 `looksLikeXMLContent` swap)
- [OPDS 2.0 JSON search greyed out (#4502)](opds2-json-search-4502.md) — `isSearchLink` ignored templated `application/opds+json` links → `hasSearch` false → disabled navbar input; add `MIME.OPDS2`+`templated`, `expandOPDSSearchTemplate` (foliate `uri-template.js`), handleSearch OPDS2 branch. Gotcha: `resolveURL` mangles `{?query}` braces — expand template BEFORE resolving
@@ -70,7 +73,9 @@
- [Double-click-drag turns page (#4524)](dblclick-drag-pageturn-4524.md) — web double-click+drag selection also turned the page; 1st click's deferred single-click (250ms) fires mid-drag while 2nd-click button held; fix = `isMouseDown` flag in `iframeEventHandlers.ts` gates the deferred `postSingleClick`; synthetic-repro gotchas (shadow-DOM iframe walk, chained-repro timing pollution, reload to re-bind listeners)
- [RSVP font face/family (#4519)](rsvp-font-settings-4519.md) — RSVP word was hardcoded `font-mono`; now mirrors the reader font via `getBaseFontFamily(viewSettings)` (new export in `style.ts`, shares `buildFontFamilyLists` with `getFontStyles`). Overlay renders in the TOP document (portal to body) where custom + basic Google fonts are mounted; known gap = built-in CJK web fonts only in top doc when `isCJKEnv()`
- [Footnote aside border line (#4438)](footnote-aside-namespace-order-4438.md) — v0.11.4 regression: stray horizontal line below footnote marker. #4383 inlined custom `@font-face` BEFORE the `@namespace epub` (which lived in `getPageLayoutStyles`), invalidating it per CSS spec → namespaced `aside[epub|type~="footnote"]{display:none}` dropped → book's `aside{border:3px double}` showed. Only with custom fonts loaded. Fix = hoist `@namespace` to front of `getStyles`. Repro needs XHTML (`epub:type` namespaced only in XML); Playwright `setContent` parses HTML and won't reproduce
- [Scrolled-mode notch mask vs texture (#4486)](notch-mask-texture-4486.md) — top inset mask occluded the bg texture; full-cell + clip-path paint-box-matching for tile alignment; CDP-inject + MAE seam verification on device; adb taps in status-bar region eaten by SystemUI
- [Paragraph-mode accidental exit + off-center bar (#4474)](paragraph-mode-accidental-exit-4474.md) — backdrop/center taps exited focus mode (stray "too high/low" taps); `ParagraphBar` only reshows on mousemove (no touch reshow) so can't just delete tap-exits → new `paragraph-show-controls` event reveals the bar instead. Also bar `absolute``fixed`: it centered on the gridcell which a pinned sidebar shifts right, while the paragraph centers on the `fixed inset-0` overlay/viewport
- [Share intent + customizable toolbar (#4014)](annotation-share-toolbar-4014.md) — Share tool in the selection toolbar (sharekit gated mobile+macOS only re: #4343 Windows freeze; `canShareText`/`shareSelectedText` in dual-purpose `share.ts`) + drag-and-drop customizer sub-page; `annotationToolbarItems` view setting (Share hidden by default); pure helpers in `annotationToolbar.ts`
## Library Fixes
- [Tauri menu append race (#4389)](tauri-menu-append-race-4389.md) — un-awaited `Menu.append()` (async IPC) in `BookshelfItem.tsx` → context-menu items shuffle order every open (native only, invisible in jsdom); fix = single `await Menu.new({ items })` of ordered `MenuItemOptions`; order/inclusion extracted to pure `getBookContextMenuItemIds` for unit testing
@@ -0,0 +1,42 @@
---
name: annotation-share-toolbar-4014
description: "Share intent in the selection toolbar + drag-and-drop toolbar customizer (#4014)"
metadata:
node_type: memory
type: project
originSessionId: 507a0166-cb55-4f33-b633-3230c0c514ff
---
#4014 — added a native "Share" tool to the in-reader text-selection toolbar plus a
drag-and-drop customizer (show/hide + reorder tools). Branch
`feat/annotation-share-toolbar-4014`; spec + plan in
`docs/superpowers/{specs,plans}/2026-06-13-annotation-share-toolbar*`.
Key facts / gotchas:
- **`src/utils/share.ts` is dual-purpose** — it already held share-LINK helpers
(`buildShareUrl`/`parseShareDeepLink` for the `/s/{token}` feature). Text-share was
added there: `shareSelectedText(text, position?, appService?)` and
`canShareText(appService)`.
- **Native share is gated to mobile + macOS only** (`isMobileApp || isMacOSApp`).
Windows/Linux desktop are excluded because `@choochmeque/tauri-plugin-sharekit-api`'s
share UI can FREEZE the app on Windows (issue #4343) — `nativeAppService.saveFile`
gates `shareFile` the same way. Ladder: native → `navigator.share` → clipboard.
`canShareText` = that OR web `navigator.share`; used to gate Share's visibility in
toolbar + customizer + the quick-action dropdown.
- **Toolbar order is a view setting**: `AnnotatorConfig.annotationToolbarItems`
(`src/types/book.ts`), default in `DEFAULT_ANNOTATOR_CONFIG` = the original 8 tools,
**Share hidden by default** (starts in the "Available" tray). No migration needed:
the `{...getDefaultViewSettings(ctx), ...saved}` merge in `settingsService.ts` +
`getToolbarToolTypes(undefined,...)` fallback both yield the default.
- **Pure helpers** in `src/utils/annotationToolbar.ts` (unit-tested) own all
order/visibility logic: `getToolbarToolTypes`/`getAvailableToolTypes` (canShare-gated,
dedup, drop-unknown), `add/remove/reorderToolbar`. `ALL_ANNOTATION_TOOL_TYPES` is
asserted to match the `annotationToolButtons` registry order by a test.
- **Customizer** = `src/components/settings/AnnotationToolbarCustomizer.tsx`, a sub-page
off `ControlPanel` (Behavior panel) via `NavigationRow`, two `@dnd-kit` zones (pattern
copied from `CustomDictionaries.tsx`). Chips are tap-to-toggle AND drag (e-ink/keyboard
a11y, since no KeyboardSensor). Cross-platform guard: when editing on a `!canShare`
device, `persist` re-appends a `share` that was synced-in but hidden, so it isn't
dropped for the user's share-capable devices.
- Adding a tool to the union (`AnnotationToolType`) is compile-checked: the
`createAnnotationToolButtons` generic in `AnnotationTools.tsx` requires every member.
@@ -0,0 +1,73 @@
---
name: dark-mode-texture-body-bg-4446
description: "#4446 dark-mode bg texture occluded by body.theme-dark opaque bg !important (style.ts getDarkModeLightBackgroundOverrides); verified on Xiaomi via CDP; multiview = patch ALL section iframes"
metadata:
node_type: memory
type: project
originSessionId: 61f864c0-488e-466c-89e9-86df66b57d42
---
RESOLVED — PR #4564 MERGED 2026-06-12 (`fix/dark-mode-texture-4446`, built on origin/main
via temp-index plumbing from the dirty dev tree; branch + local mods cleaned after merge).
User-verified on the Xiaomi via `pnpm dev-android` (full aarch64 build + `adb install -r`,
~7 min).
#4446 remaining case (after [[paginated-texture-occlusion-4399]] fixed light mode): in
**dark mode** the bg texture is absent in paginated entirely, and absent in the scrolled
text area while header/footer still show it.
**Root cause (verified live on Xiaomi 2211133C via CDP, no rebuild).**
`getDarkModeLightBackgroundOverrides` (style.ts ~194) emits
`body.theme-dark { background-color: ${bg} !important; }`, applied when
`isDarkMode && !overrideColor` (style.ts ~320). That paints every section iframe's body
opaque dark (`rgb(34,34,34)`), which occludes the host `.foliate-viewer::before` texture.
Downstream it also poisons foliate: `resolveBackground(view.docBackground)` resolves the
opaque body color, so `textureAwareBackground` keeps the paginated `#background` segment
and the scrolled `view.element` inline bg opaque too. The #4399 fix is intact (container
is transparent); this is a second, dark-mode-only occluder one layer deeper.
- Paginated: segment + iframe body span the full viewer → no texture anywhere.
- Scrolled: iframes cover only the text column → header/footer strips keep texture
(grid-cell + the #4486 notch overlay, see [[notch-mask-texture-4486]]).
**Proof:** injecting `body.theme-dark{background-color:transparent !important}` into ALL
section iframes + clearing the segment/view inline bgs reveals the leaves texture in both
modes instantly.
**Regression source (git-proven):** commit `176b950c9` = PR #4392 (2026-06-01, shipped
v0.11.4) added the rule. NOT foliate-js — the foliate swipe-flash regression (#4399,
167757a→142bf11) broke LIGHT-mode textures in the same release window, which is why it
looked like one. The transformStylesheet light-bg rewriter (also #4392) was EXONERATED
for the repro book: Alice's stylesheets have zero body/html background rules (verified by
on-device stylesheet enumeration). **No #4392 revert needed** — its callout attribute
selectors + rewriter fix real legibility bugs (#4028; #4419/#4426 build on them).
**Fix (applied):** make the rule `background-color: transparent !important`
UNCONDITIONALLY, not gated on hasBackgroundTexture, because foliate captures
`docBackground` once per section load (paginator.js `load` listener; `setStyles` re-runs
`#replaceBackground` but never re-captures), so a texture-gated body bg would go stale on
live texture toggling. Visuals without texture are identical: the dark fill comes from the
paginator container `fallbackBg` / reader grid cell. Book-forced light page bgs stay
neutralized (theme-dark fill shows through); page-level rewriter output is cascade-beaten
by our `!important` body rule and later-in-head `html` rule; only book `!important`
page rules survive — consistent with the #4399 "book-forced opaque page wins" policy.
Test: `style-get-styles.test.ts` "#4446" cases. E2E device-verified: with the fixed CSS
present at load, capture is transparent and the paginated segments array comes out EMPTY.
**Verification gotchas (cost ~30 min):**
- **Multiview!** The renderer shadow root holds MULTIPLE section iframes (adjacent preload).
Patching `sr.querySelector('iframe')` hits a section possibly 18k px off-viewport →
"fix didn't work". Patch every `sr.querySelectorAll('iframe')`.
- `elementsFromPoint` reports the iframe ELEMENT's computed bg (transparent) — the
occluding paint is its content document's body, invisible to the top-doc/shadow stack walk.
- Switching `renderer.setAttribute('flow', ...)` can reload section docs and silently wipe
styles injected into them (but not always — re-check after every flow switch).
- Pseudo-element paint test: patch the `#background-texture` style text with
`background-color: red` — if red doesn't show, the pseudo is occluded, not broken.
- **Stale preload views**: after patching styles via `renderer.setStyles`, views loaded
PRE-patch keep their opaque `docBackground` and `#clearViewsExcept` keeps `|iindex|≤2`
across navigation — an opaque segment can come from a kept old view, not the fresh one.
Jump ≥3 sections to guarantee fresh captures.
- **Capture-time instrumentation**: the paginator dispatches `load` synchronously right
BEFORE `docBackground = getBackground(doc)` — an event listener on the renderer sees
exactly what the capture will see (class list, computed bgs, active rules).
@@ -0,0 +1,45 @@
---
name: dict-import-contenturi-filename-4489
description: "Android dict import \"incomplete bundle\" — ext-less content URIs; classify used getFilename (string) not basename (content resolver)"
metadata:
node_type: memory
type: project
originSessionId: 92f1011c-89b9-4ae1-b128-f24cfb4462a8
---
#4489 / #4472: importing a StarDict bundle (`.ifo`+`.idx`+`.dict.dz`) on **some** Android
devices fails with "Skipped incomplete bundles"; works on Xiaomi/Boox and web.
**Root cause — two filename resolvers diverged.** Tauri's Android `path.file_name`
(used by `basename`) special-cases `content://`/`file://` URIs and calls the native
`getFileNameFromUri` plugin → **content resolver DISPLAY_NAME** (real filename WITH ext).
See `tauri-2.11.2/src/path/android.rs`. Our `getFilename()` (`src/utils/path.ts`) is pure
JS string-parse of the URI. On devices whose SAF URI is an **opaque ext-less document id**
(e.g. `content://com.android.providers.downloads.documents/document/msf%3A20`), getFilename
`msf%3A20` (no ext) while basename → `21cen.dict.dz`. Working devices return
`primary%3ADictionaries%3A21cen.dict.dz` (ext in the URI) so getFilename happens to work.
The OLD `selectFileTauri` extension filter ALREADY used `basename` (so files passed the
filter and got imported), but **threw the resolved name away** and returned the raw URI.
Then `dictionaryService.classify()` re-derived the name with `getFilename()` → no ext →
every file orphaned → "incomplete bundle". The bug is the divergence, not the picker.
**Fix (PR for #4489):**
- `SelectedFile.name?: string` added (`useFileSelector.ts`).
- `resolveTauriFileName(path, appService)`: `basename` for `content://` / iOS `file://`,
else `getFilename` — resolved ONCE in `selectFileTauri`, reused by the ext filter AND
stored on `SelectedFile.name`. Removed `processTauriFiles`.
- `classify()` (`dictionaryService.ts`): `source.file?.name ?? source.name ?? getFilename(path)`.
- Test: `groupBundlesByStem.test.ts` — ext-less content URIs + `name` form a complete bundle.
- Also fixes #4472 issue 3 (uploaded dict files renamed to the SAF dir path).
**Emulator repro (real granted URI, no rebuild needed):** drive SAF picker → "Downloads"
location → returns `content://...downloads.documents/document/msf%3A<id>` (ext-less).
basename resolves the real name ONLY for granted URIs (synthetic URIs → "path does not have
a basename"). Verified via CDP `invoke('plugin:path|basename',{path,ext:null})`. See
[[cdp-android-webview-profiling]] for the CDP harness (`src/__tests__/android/helpers/`).
**Also fixed same turn:** e-ink "black spot" on the Settings→Dictionaries `+` badges
(Import Dictionary / Add Web Search) — add `eink-inverted` to the round badge span, mirroring
the font import button #4454 (`globals.css` `[data-eink] .eink-inverted` → base-content bg +
base-100 icon).
@@ -0,0 +1,17 @@
---
name: dict-lookup-browser-hijack-4559
description: Android system-dictionary lookup landing in the OEM browser instead of Eudic/欧路 — package-visibility + PROCESS_TEXT browser hijack
metadata:
node_type: memory
type: project
originSessionId: 5e397668-b766-439c-873c-00ccb1da715a
---
#4559 (PR #4568): on VIVO/iQOO (OriginOS) the system-dictionary lookup opened `com.vivo.browser/.BrowserActivity` instead of an installed dictionary. TWO root causes, both in the Android half of `show_lookup_popover` (`tauri-plugin-native-bridge/.../NativeBridgePlugin.kt`):
1. **Package-visibility filtering (primary).** App is `targetSdk 36`, but the native-bridge manifest had NO `<queries>` for `ACTION_PROCESS_TEXT`. Under Android 11+ filtering, `queryIntentActivities(PROCESS_TEXT)` then returns only *auto-visible* apps — web browsers are auto-visible (web-intent exception), arbitrary dictionary apps (Eudic/欧路/GoldenDict) are NOT. So the query returned just the browser. Fix = add `<queries><intent><action PROCESS_TEXT/><data text/plain/></intent></queries>` to the plugin manifest (mirrors the native-tts `TTS_SERVICE` pattern). This alone is likely the whole user-visible fix.
2. **Browser hijack.** Even when visible, an OEM browser registering `ACTION_PROCESS_TEXT` can be the system default and swallow a plain `startActivity`. Fix = filter browsers out of the handler set in a pure `decideLookupDispatch(handlers, browserPackages, remembered)` (new `LookupDispatch.kt`, JUnit-tested): no-browser → implicit (unchanged, keeps native "Always"); browser+1 dict → explicit `setClassName` direct launch; browser+≥2 → `createChooser` + `EXTRA_EXCLUDE_COMPONENTS`; browser-only → `unavailable:true`. Browsers detected via `queryIntentActivities(ACTION_VIEW https + BROWSABLE)` (auto-visible, no `<queries>` needed).
**Remember-the-choice** (the maintainer wanted "smooth once chosen"): `ACTION_CHOOSER` has NO native "Always" button (mutually exclusive with `EXTRA_EXCLUDE_COMPONENTS` — the resolver that *has* Always can't exclude and obeys the browser default). Re-implemented Always: pass an `IntentSender` to `createChooser`; system returns `EXTRA_CHOSEN_COMPONENT` to a manifest `LookupChoiceReceiver` (exported=false; explicit intra-app PendingIntent so non-exported is fine; FLAG_MUTABLE on S+) which persists pkg/class to a plain SharedPreferences (`readest_lookup_dictionary_v1`). Next lookup fast-paths it. Reset UI: `get_lookup_dictionary`/`clear_lookup_dictionary` commands (build.rs COMMANDS + default.toml + autogenerated TOMLs/schema/reference regenerate on `cargo build -p tauri-plugin-native-bridge`) → Android-only conditional reset row in `CustomDictionaries.tsx`, only shown when something is actually remembered (`getRememberedLookupApp` returns null otherwise, so no clutter).
Maintainer DECLINED a settings picker to pre-pick a specific app ("we won't call the app directly"); browser-exclusion respects that (dynamic, not a user-set hardcode). Verified: gradle JUnit (7 cases) + vitest (12). `test:rust`/`clippy` were blocked by UNRELATED stale shared-`target/` cache (deleted sibling worktree `readest-feat-android-rangefile-protocol` path in `fs` plugin permission scan) — not my change; validated Rust via targeted plugin build. Related: [[android-open-with-intent-flow]], [[android-nativefile-remotefile-io]].
@@ -0,0 +1,23 @@
---
name: edge-tts-word-highlighting-4017
description: "Edge TTS word-by-word highlighting (#4017, PR"
metadata:
node_type: memory
type: project
originSessionId: afd9b381-c17d-4988-b287-07263d8bea0b
---
# Edge TTS word-by-word highlighting (#4017, PR #4566)
**Design: keep sentence granularity, add word highlight on top.** All clients still report `getGranularities() = ['sentence']` — switching foliate to word marks would regress media-session metadata (one word on lock screen), byMark seek (word steps), `getSpokenSentence`, and per-word synthesis. Instead: `EdgeSpeechTTS.createAudio()` returns `{url, boundaries}` (cached per payload-hash next to the blob URL), `EdgeTTSClient` runs a rAF loop syncing `audio.currentTime` (media time → playbackRate/pause-safe) against boundary ticks, and `TTSController.prepareSpeakWords/dispatchSpeakWord` match words sequentially (`indexOf` with a moving cursor; unmatched word = skip WITHOUT advancing cursor) against the sentence range text, then highlight the sub-range via the existing `#getHighlighter`.
**Edge wire facts** (verified with raw WS probe + live):
- `audio.metadata` frames: `{"Metadata":[{"Type":"WordBoundary","Data":{"Offset":1000000,"Duration":4250000,"text":{"Text":"Dr.","Length":3}}}]}` — one word/frame, ticks = 100 ns (1e7/s), offsets relative to this request's audio stream.
- `Text` is the **verbatim input span** ("Dr.", "23", "$5.50" keep punctuation; trailing sentence punctuation stripped) → sequential indexOf matching is robust. Works for zh too.
- The readaloud endpoint gates on **User-Agent (needs Edg/non-headless), NOT Origin** — a localhost Origin with Edg UA is accepted; default HeadlessChrome UA is rejected (close 1006).
**Pre-existing bug fixed in the same PR:** browser branch did `new WebSocket(url, {headers})` → native WebSocket parses the object as a subprotocol → `SyntaxError` → on web the wss path could NEVER work (always https-proxy fallback, which strips boundaries). Node-only options now.
**Probe gotchas:** Overlayer draws the highlight as a `<path>` inside `<g fill="#808080">` (NOT `<rect>` — rect-only DOM probes miss it); the overlayer svg lives in `FOLIATE-PAGINATOR`'s open shadow root (sibling layer of the iframe, not inside it). TTS auto-advance creates new views — re-query svgs per sample, never cache the list.
**dev-web live-verify recipe:** gstack `browse --proxy http://127.0.0.1:8118` (flag needed on EVERY invocation; this machine's external net needs the local proxy, headless Chromium doesn't inherit it) + `browse useragent '...Edg/143...'` (context-level, doesn't break Next) — do NOT use `browse header Origin:...` (extra headers hit localhost too → Next dev 403s ALL chunks → blank page; headers can't be removed without daemon restart). Import books via synthetic drop: fetch epub from `public/`, `DataTransfer` + `DragEvent('drop')` on `.library-page` (in-memory only — re-import after every reload/restart). Patch `content.overlayer.add/remove` to log the real highlight calls — the ground truth when screenshots race. Related: [[tts-fixes]]
@@ -0,0 +1,40 @@
---
name: notch-mask-texture-4486
description: "Scrolled-mode top inset mask occluded the bg texture; clip-path full-cell trick aligns the mask's texture tiles with the viewer's; CDP-inject + MAE seam verify"
metadata:
node_type: memory
type: project
originSessionId: 47d2276d-0e04-455c-99b5-4fd0a651b579
---
#4486 (PR #4563): in scrolled mode the `notch-area` in `SectionInfo.tsx` masks the top
safe-area inset with opaque `bg-base-100` at z-10 (hides content scrolling under the
status bar) — and painted over the texture (`.foliate-viewer::before` lives at the z-0
paint layer, see [[paginated-texture-occlusion-4399]]). Flat untextured strip across the
unsafe header area.
**Fix pattern (paint-box matching).** A texture `::before` only tile-aligns with the
viewer's when `background-size: cover/contain` resolves against the SAME element box.
So: make the mask span the grid cell (`inset-0`) and clip the visible+hit area to the
strip with `clip-path: inset(0 0 calc(100% - topInsetPx) 0)`; add `.notch-masked::before`
to the selector group in `styles/textures.ts`, gated by a conditional class only in
scrolled-horizontal mode (paginated/vertical notch is transparent — texturing it would
double-texture over the viewer's). `mix-blend-mode: multiply` blends against the mask's
own opaque bg inside its z-10 stacking context → identical color math to the viewer area.
clip-path clips hit-testing too, so the click target stays the strip (verified with
`elementsFromPoint`: notch present in stack inside strip, absent mid-screen).
**Still texture-unaware** (same flaw, not yet reported): the vertical scrolled-mode side
masks in `BooksGrid.tsx` (`bg-base-100 absolute left-0/right-0 h-full` when
vertical+scrolled). Same trick applies if reported.
**Verification technique (no rebuild).** Drive the installed app on the device via
adb+CDP ([[cdp-android-webview-profiling]]) and inject the exact CSS artifacts the fix
produces (patch the `#background-texture` style text + classList/style edits — React
won't wipe manual DOM edits unless its computed className/style prop string changes
between renders). Quantify the seam: `magick compare -metric MAE` between adjacent
1px rows across the boundary — buggy hard seam was MAE 11913 (16-bit scale), fixed
seam 230 ≈ ordinary texture-row variation. Gotchas: adb taps at status-bar y-coords are
consumed by SystemUI (never reach the app) — use elementFromPoint or in-page click
counters for hit-testing; a mid-screen tap toggles the header/footer chrome which then
sits ABOVE the notch and pollutes hit stacks (toggle it off before probing).
@@ -39,6 +39,7 @@ TTS tracks its own section independently from the view via `#ttsSectionIndex`:
| #3291 | TTS fails without lang attribute | Invalid SSML from missing lang | Set lang/xml:lang on html element from `ttsLang` |
| #3292 | Can't restart TTS from annotation | `ttsOnRef` blocks re-entry | Removed the guard ref entirely |
| #3400 | TTS highlight stops after restart | `view.tts` not nulled on shutdown | Added `this.view.tts = null` in `shutdown()` |
| #4033 | Voice count flip-flops within one book (17↔5) | All 3 clients filtered voices by full locale (`v.lang.startsWith(locale)`); panel lang refreshes from the speaking mark (`getSpeakingLang`), and books mix region variants — Standard Ebooks boilerplate is `en-US` (17 Edge voices), body `en-GB` (5 Edge voices) | PR #4565: filter by primary lang (`isSameLang`) in Edge/Web/Native `getVoices`; new `TTSUtils.sortVoicesPreferLocaleFunc(locale)` keeps exact-locale voices first so `getVoiceIdFromLang` default stays region-aware. Also fixed `zh-Hans` → empty Edge list |
## Debugging TTS Issues