Compare commits

...

12 Commits

Author SHA1 Message Date
sudacode 6af9a19ff3 feat(overlay): add primary subtitle bar visibility modes
- Cycle `v` through `hidden | visible | hover` instead of a boolean toggle
- Add `subtitleStyle.primaryDefaultMode` config with default `visible`
- Carry primary mode independently from secondary in hot-reload payload
- Add hover CSS: transparent until hovered, then fully visible
- Show primary-specific OSD text on each mode change
2026-05-12 20:30:42 -07:00
sudacode 430373f010 feat(tokenizer): use Yomitan word classes for subtitle POS filtering (#57)
* feat(tokenizer): use Yomitan word classes for subtitle POS filtering

- Carry matched headword wordClasses from termsFind into YomitanScanToken
- Map recognized Yomitan wordClasses to SubMiner coarse POS before annotation
- MeCab enrichment now fills only missing POS fields, preserving existing coarse pos1
- Exclude standalone grammar particles, して helper fragments, and single-kana surfaces from annotations
- Respect source-text punctuation gaps when counting N+1 sentence words
- Preserve known-word highlight on excluded kanji-containing tokens
- Add backlog tasks 304 (N+1 boundary bug) and 305 (wordClasses POS, done)

* fix(tokenizer): preserve annotation and enrichment behavior

* fix: restore jlpt subtitle underlines

* fix: exclude kana-only n+1 targets

* fix: refresh overlay on Hyprland fullscreen

* fix: address fullscreen and n-plus-one review notes

* fix: address CodeRabbit review comments

* fix: accept modified digits for multi-line sentence mining

* Cancel pending Linux MPV fullscreen overlay refresh bursts

- return a cancel handle from the Linux refresh burst scheduler
- clear pending refresh bursts when overlays hide or windows close
- tighten the burst test polling to wait for the async refresh

* fix: suppress N+1 for kana-only candidates and fix minSentenceWords coun

- Treat kana-only tokens with surrounding subtitle punctuation (…, ―, etc.) as kana-only so they are not promoted to N+1 targets
- Exclude unknown tokens filtered from N+1 targeting from the minSentenceWords count so filtered kana-only unknowns cannot satisfy sentence length threshold
- Add regression tests for kana-only candidate suppression and filtered-unknown padding cases

* Suppress subtitle annotations for grammar fragments

- Hide annotation metadata for auxiliary inflection and ja-nai endings
- Preserve lexical `くれる` forms and add regression coverage

* Fix kana-only N+1 tokenizer regression test

- Use a pure-kana fixture for the subtitle token N+1 case
- Update task notes for the latest CodeRabbit follow-up

* Fix managed playback exit and tokenizer grammar splits

- Ignore background stats daemons during regular app startup
- Split standalone grammar endings before applying annotations
- Clear helper-span annotations for auxiliary-only tokens

* fix: refresh current subtitle after known-word mining

* fix: suppress sigh interjection annotations

* fix: preserve jlpt underline color after lookup

* Replace grammar-ending permutations with shared matcher; preserve word a

- Extract `grammar-ending.ts` with `isStandaloneGrammarEndingText` / `isSubtitleGrammarEndingText` pattern matchers
- Replace `STANDALONE_GRAMMAR_ENDINGS` set in parser-selection-stage with shared matcher
- Replace generated phrase sets in subtitle-annotation-filter with shared matcher
- Remove stale duplicate subtitle-exclusion constants and helpers from annotation-stage
- Manual clipboard card updates now write only to the sentence audio field, leaving word/expression audio untouched

* fix: CI changelog, annotation options threading, and Jellyfin quit

- Add `type: fixed` / `area:` frontmatter to `changes/319` to pass `changelog:lint`
- Thread `TokenizerAnnotationOptions` through `stripSubtitleAnnotationMetadata` so `sourceText` is honored
- Include `jellyfinPlay` in `shouldQuitOnDisconnectWhenOverlayRuntimeInitialized` predicate
- Make mouse test `elementFromPoint` stubs coordinate-sensitive
- Make Lua test `.tmp` mkdir portable on Windows

* Preserve overlay across macOS flaps and mpv playlist changes

- keep visible overlays alive during transient macOS tracker loss
- reuse the running mpv overlay path on playlist navigation
- update regression coverage and changelog fragments

* fix: restore stats daemon deferral

* fix: keep subtitle prefetch alive after cache hits

* Fix JLPT underline color drift and AniList skipped-threshold sync

- Replace JLPT `text-decoration` underlines with `border-bottom` so Chromium selection/hover cannot repaint them to another annotation's color
- Lock JLPT underline color for combined annotation selectors (known, n+1, frequency) and character hover/selection states
- Trigger AniList post-watch check on every mpv time-position update to catch skipped completion thresholds
- Fall back to filename-parser season/episode when guessit omits them

* fix: address coderabbit feedback

* fix: sync AniList after seeked completion

* fix: preserve ordinal frequency annotations

* fix: preserve known highlighting for filtered tokens

* fix: address PR #57 CodeRabbit feedback

- Acquire AniList post-watch in-flight lock before async gating to prevent duplicate writes
- Isolate manual watched mark result from AniList post-watch callback failures
- Report known-word cache clears as mutations during immediate append when state existed
- Add regression tests for each fix

* fix: stop AniList setup reopening on Linux when keyring token exists

- Gate setup success on token persistence: `saveToken` now returns `boolean`; on failure, keeps the setup window open instead of reporting success
- Config reload passes `allowSetupPrompt: false` so playback reloads don't re-open the setup window
- Add regression test for persistence-failure path

* fix: suppress known highlights for subtitle particles

* fix: retry transient AniList safeStorage failures

* fix: hide overlay focus ring

* fix: align Hyprland fullscreen overlays

* fix: restore subtitle playback keybindings

* fix: align Hyprland overlay windows to mpv and stop pinning them

- Force-apply exact Hyprland move/resize/setprop dispatches when bounds are provided
- Stop pinning overlay windows; toggle pin off when Hyprland reports pinned=true
- Compensate stats overlay outer placement for Electron/Wayland content insets
- Make stats overlay window and page opaque so mpv cannot show through transparent insets
- Constrain stats app to h-screen with internal scroll so content covers mpv from y=0
- Lock overlay/stats window titles against page-title-updated events
- Add regression coverage for placement dispatches, inset compensation, and CSS overlay mode

* fix: retain frequency rank for honorific prefix-noun tokens

- Add `shouldAllowHonorificPrefixNounFrequency` to exempt お/ご/御 + noun merged tokens from frequency exclusion
- Add regression test for `ご機嫌` asserting rank 5484 is preserved after MeCab enrichment and annotation
- Close TASK-341

* fix: map openCharacterDictionary session action to --open-character-dict

- Add missing Lua CLI dispatch entry for openCharacterDictionary
- Add regression test for Alt+Meta+A binding and CLI flag forwarding

* fix: keep macOS overlay interactive while mpv remains active

- Overlay no longer hides or becomes click-through during tracker refreshes when mpv is the focused window
- Preserve already-visible overlay when tracker is temporarily not ready but mpv target signal is active
- Add regression tests for active-mpv tracker refresh and transient tracker-not-ready paths

* fix: address coderabbit subtitle follow-ups

* fix: resolve media detail from sessions when lifetime summary is absent

- Change `getMediaDetail` JOIN to LEFT JOIN on `imm_lifetime_media` and fall back to aggregated session metrics when no lifetime row exists
- Add filter `AND (lm.video_id IS NOT NULL OR s.session_id IS NOT NULL)` to keep results valid
- Add regression test covering the session-visible / media-detail-missing mismatch

* fix: address PR-57 CodeRabbit findings and CI failures

- use filtered word counts in media detail session token aggregation
- cancel fullscreen refresh burst on exit via updateLinuxMpvFullscreenOverlayRefreshBurst
- guard Hyprland JSON.parse in try/catch; exclude windowtitle from geometry events
- narrow focus suppression from :focus to :focus-visible
- apply JLPT lock selectors to word-name-match tokens (N1–N5)

* fix: macOS overlay z-order and Yomitan compound token known highlighting

- Release always-on-top when tracked mpv loses foreground on macOS
- Skip visible overlay blur restacking on macOS to avoid covering unrelated windows
- Prefer Yomitan internal parse tokens over fragmented scanner output for known-word decisions
- Add regression tests for both behaviors

* fix: macOS visible-overlay blur no longer invokes Windows-only blur call

- Split win32/darwin branches in handleOverlayWindowBlurred so darwin visible blur returns early without calling onWindowsVisibleOverlayBlur
- Add regression test asserting Windows callback stays inactive on macOS visible overlay blur
- Close TASK-347
2026-05-12 12:08:09 -07:00
sudacode b68d17614d Add canonical URLs and sitemap dedup for docs homepage indexability
- Emit self-referential canonical `<link>` tags on every VitePress page
- Filter duplicate /README entry from generated sitemap
- Extract DOCS_HOSTNAME constant; update Plausible test to match
- Add seo.test.ts covering canonical generation and sitemap filtering
2026-05-10 22:19:21 -07:00
sudacode 30712738dc Fix Yomitan popup shortcut precedence in keyboard-only mode (#61) 2026-05-04 00:06:06 -07:00
sudacode 0915b23dc8 Persist stats exclusions in DB and fix word metrics filtering (#60) 2026-05-03 20:06:13 -07:00
sudacode db30c61327 [codex] Fix Jellyfin setup and discovery toggle (#59) 2026-05-02 19:56:10 -07:00
sudacode 27f5b2bb58 Polish changelog fragments with claude -p at release time
- Replace `renderGroupedChanges` with `polishFragmentsWithClaude` that pipes fragments through `claude -p --model sonnet` to merge related items, drop housekeeping noise, and produce user-facing release notes
- Internal fragments kept in CHANGELOG.md under a `<details>` collapse; dropped from GitHub release notes entirely
- CI no longer auto-runs `changelog:build` on tag-based releases — fails fast with a clear error if `changes/*.md` fragments are still pending; build locally and commit before tagging
- Add `runClaude` dep-injection seam to test surface; add failure-mode coverage (missing binary, empty output, missing headers, missing `<details>` wrapper)
- Delete implemented design doc; update `changes/README.md` and `docs/RELEASING.md` with claude CLI prerequisite and new workflow
2026-05-02 19:52:48 -07:00
sudacode baabdb6d30 Add design doc for AI-polished changelog workflow
- Capture decisions from brainstorming: replace bullet renderer with `claude -p`, write straight to disk, hard-fail on missing/failed claude, drop internal section from release notes but keep collapsed in CHANGELOG.md
- Document prompt input/output contract, affected files, test plan, and CI guard that fails tag-based releases when changelog fragments are still pending
- Set scope boundaries (no caching, no SDK fallback, no `--no-polish` escape hatch)
2026-05-02 19:52:13 -07:00
sudacode 3a67e23bc3 feat: open texthooker from cli and tray 2026-05-02 19:37:44 -07:00
sudacode 13e2b5f8c8 Handle mpv reload buffering as same media
- Keep overlay alive across same-media mpv reloads
- Avoid rearming startup gate and repeating AniSkip lookups
- Add regression coverage for reload/end-file/file-loaded sequence
2026-05-02 15:42:54 -07:00
sudacode 53aa58d044 Route stats background mode through isolated daemon and defer in-app startup to live daemon (#58) 2026-04-26 19:26:01 -07:00
sudacode d8934647a9 Restore multi-copy digit capture and add AniList selection (#56) 2026-04-25 21:44:55 -07:00
374 changed files with 17036 additions and 1139 deletions
+3 -4
View File
@@ -351,12 +351,11 @@ jobs:
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Build changelog artifacts for release
- name: Guard against pending changelog fragments
run: |
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then
bun run changelog:build --version "${{ steps.version.outputs.VERSION }}"
else
echo "No pending changelog fragments found."
echo "::error::Pending changelog fragments detected. Run 'bun run changelog:build --version ${{ steps.version.outputs.VERSION }}' locally and commit the polished CHANGELOG.md before tagging. CI no longer auto-builds the changelog because the polish step requires the local 'claude' CLI."
exit 1
fi
- name: Verify changelog is ready for tagged release
+3 -1
View File
@@ -84,7 +84,7 @@ Browse sibling episode files and the active mpv queue in one overlay modal. Open
</tr>
<tr>
<td><b>Jellyfin</b></td>
<td>Browse and launch media from your Jellyfin server</td>
<td>Browse, launch, and cast media from your Jellyfin server with setup and discovery controls in the app tray</td>
</tr>
<tr>
<td><b>Jimaku</b></td>
@@ -252,6 +252,8 @@ subminer app --setup # launch the first-run setup wizard
SubMiner creates a default config, starts in the system tray, and opens a setup popup that walks you through installing the mpv plugin and configuring Yomitan dictionaries. Follow the on-screen steps to complete setup.
Jellyfin setup is available from the tray or `subminer jellyfin`; once Jellyfin is enabled with a server URL, the tray can toggle Jellyfin Discovery for the current app session.
> [!NOTE]
> On Windows, run `SubMiner.exe` directly — it opens the setup wizard automatically on first launch.
@@ -0,0 +1,44 @@
---
id: TASK-314
title: Improve Jellyfin setup popup and tray discovery toggle
status: Done
assignee: []
created_date: '2026-05-02 22:45'
updated_date: '2026-05-02 23:11'
labels:
- jellyfin
dependencies: []
references:
- src/main/runtime/jellyfin-setup-window.ts
- src/main/runtime/jellyfin-cli-auth.ts
- src/main/runtime/tray-runtime.ts
- src/main/runtime/jellyfin-remote-session-lifecycle.ts
documentation:
- docs-site/jellyfin-integration.md
- docs-site/configuration.md
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Improve the Jellyfin integration setup experience and remove the need to use command-line discovery mode for normal tray-driven use. The existing `--jellyfin` setup popup should become a frontend for the same auth persistence path used by CLI login, with manual/recent server selection and inline feedback. The tray should expose a runtime-only Jellyfin Discovery checkbox when Jellyfin is configured so users can start or stop cast/discovery mode without changing config.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The Jellyfin setup popup supports config/recent/default server choices, manual URL entry, username/password login, logout when a session exists, done/close, and inline success/error status without persisting passwords.
- [x] #2 CLI login and setup popup login share the same auth persistence behavior, including encrypted token storage, enabled/server/username/client metadata config patching, and recent server updates.
- [x] #3 `jellyfin.recentServers` is parsed, normalized, deduplicated, capped, documented, and included in generated config examples if exposed.
- [x] #4 The tray keeps Configure Jellyfin visible and shows a Jellyfin Discovery checkbox only when Jellyfin is configured with enabled integration, server URL, access token, and user ID.
- [x] #5 The tray Jellyfin Discovery checkbox starts/stops the current remote session at runtime only, announces after start, reports OSD/log status, and does not patch config.
- [x] #6 Startup auto-connect behavior remains governed by existing config, including `remoteControlAutoConnect`; explicit tray start can start discovery without requiring `remoteControlAutoConnect`.
- [x] #7 Focused tests cover setup popup actions/rendering, shared auth persistence, config parsing, tray toggle visibility/state/click behavior, and remote lifecycle auto-connect versus explicit-start behavior.
- [x] #8 Jellyfin docs and changelog fragment are updated.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented Jellyfin setup popup improvements, shared auth persistence for CLI/setup config shape, recent server config support, runtime-only tray Jellyfin Discovery toggle, docs/config examples, and changelog fragment. Verified focused Jellyfin/tray tests, config tests, launcher tests, typecheck, and docs tests.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,76 @@
---
id: TASK-336
title: Fix Hyprland fullscreen overlay downward offset
status: Done
assignee: []
created_date: '2026-05-04 05:42'
updated_date: '2026-05-04 06:10'
labels:
- linux
- hyprland
- overlay
- bug
dependencies: []
references:
- src/window-trackers/hyprland-tracker.ts
- src/core/services/overlay-window-bounds.ts
- src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
SubMiner visible overlay is slightly below mpv when mpv is fullscreen on Linux Hyprland. Align overlay bounds with mpv fullscreen client/monitor bounds.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Hyprland fullscreen mpv overlay uses top-aligned geometry instead of inheriting a downward offset.
- [x] #2 Regression coverage captures the fullscreen Hyprland geometry case.
- [x] #3 Targeted tests pass.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Added follow-up Hyprland placement handling after the fullscreenClient geometry fix. SubMiner overlay/stats windows now get stable titles and, on Hyprland, are resolved from `hyprctl -j clients` by current PID/title, then set floating before bounds are applied. The stats overlay reapplies bounds after showing because Hyprland cannot see the hidden window before it is mapped.
2026-05-04 follow-up: offset remains after removing pinning. User reports stats modal still has a top gap from mpv in Hyprland fullscreen. Need inspect exact stats overlay CSS/window bounds after float-only placement.
2026-05-04 follow-up fix: stats CSS already had zero body margin, so the remaining gap points at native Hyprland placement after float-only handling. Added exact `movewindowpixel`/`resizewindowpixel` Hyprland dispatches using the same tracked mpv bounds passed to Electron.
2026-05-04 second follow-up: live `hyprctl -j clients` showed the SubMiner client was already full monitor size at `[0,0]`, so the remaining visible top strip was inside Electron's transparent stats surface rather than compositor geometry. Made the stats overlay BrowserWindow opaque with the stats base background. Also prevented page titles from overwriting the stable SubMiner overlay/stats titles used for Hyprland client matching.
2026-05-04 third follow-up: user confirmed native overlay placement is correct and the remaining gap is stats-page-specific. Made stats overlay mode paint an opaque full-viewport root/background and constrained the stats app to `h-screen` with an internal scrolling main pane, so the overlay page itself covers the mpv frame from y=0.
2026-05-04 fourth follow-up: live Hyprland data showed mpv and SubMiner shared the same outer geometry while stats content still rendered lower. Stats window placement now compensates for Electron/Wayland content insets using `getContentBounds()` versus `getBounds()`, then sends the adjusted outer bounds to Hyprland exact placement so the content area, not just the native surface, aligns to mpv.
2026-05-04 fifth follow-up: user confirmed the offset is Hyprland-fullscreen-only and not present while mpv is windowed. Added Hyprland `setprop` decoration cleanup during exact overlay placement (`rounding 0`, `border_size 0`, `no_shadow 1`, `no_blur 1`, `decorate 0`) because fullscreen mpv has square fullscreen edges while a floating SubMiner stats window can retain Hyprland floating-window decoration.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Treated Hyprland `fullscreenClient` as a fullscreen signal when resolving mpv overlay geometry.
- Added Hyprland window placement handling so SubMiner overlay/stats windows are set floating before bounds are applied.
- Added exact Hyprland move/resize dispatches so floating overlay/stats windows are force-aligned to the tracked mpv bounds.
- Gave overlay/stats windows stable titles for Hyprland client matching, and reapplied stats bounds after show.
- Locked overlay/stats window titles against page title changes and made the stats overlay window opaque so mpv cannot show through transparent Electron insets.
- Made the stats overlay page paint an opaque full-viewport background and added CSS regression coverage for overlay mode.
- Compensated stats overlay outer placement for Electron/Wayland content insets.
- Disabled Hyprland floating-window decoration for exact overlay placement over fullscreen mpv.
- Added regression coverage for the 28px fullscreen geometry shape and Hyprland placement dispatches.
- Added a changelog fragment for the overlay fix.
Verification:
- `bun test src/core/services/hyprland-window-placement.test.ts src/core/services/overlay-window-config.test.ts src/core/services/stats-window.test.ts src/core/services/overlay-window-bounds.test.ts src/window-trackers/hyprland-tracker.test.ts`
- `bun run typecheck`
- `bun run changelog:lint`
- `bun run test:fast`
- `bun test stats/src/styles/globals.test.ts stats/src/lib/api-client.test.ts src/core/services/stats-window.test.ts`
- `bun run build:stats`
- `bun test src/core/services/stats-window.test.ts src/core/services/hyprland-window-placement.test.ts stats/src/styles/globals.test.ts`
- `bun test src/core/services/hyprland-window-placement.test.ts src/core/services/stats-window.test.ts stats/src/styles/globals.test.ts`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,53 @@
---
id: TASK-339
title: Stop pinning Hyprland overlay windows
status: Done
assignee: []
created_date: '2026-05-04 06:07'
updated_date: '2026-05-04 06:09'
labels:
- linux
- hyprland
- overlay
- bug
dependencies: []
references:
- src/core/services/hyprland-window-placement.ts
- src/core/services/overlay-window.ts
- src/core/services/stats-window.ts
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Recent Hyprland placement fix pins SubMiner overlay/stats windows, making them follow across workspaces instead of staying attached to mpv. Keep the float-for-bounds behavior, but never pin overlay windows.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Hyprland placement dispatches set floating state only and does not dispatch pin.
- [x] #2 Regression coverage proves pinned clients are unpinned or at least not re-pinned by SubMiner.
- [x] #3 Targeted tests and typecheck pass.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Changed Hyprland placement dispatch construction so unpinned overlay windows only get `setfloating`; pinned overlay windows get a single `pin` dispatch to toggle the bad prior pinned state off. This preserves floating placement for bounds while keeping overlay windows workspace-local with mpv.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Stopped re-pinning Hyprland overlay/stats windows during placement.
- Added cleanup behavior for previously pinned SubMiner windows by toggling pin only when Hyprland reports `pinned: true`.
- Updated regression coverage and added a changelog fragment.
Verification:
- `bun test src/core/services/hyprland-window-placement.test.ts src/core/services/overlay-window-config.test.ts src/core/services/stats-window.test.ts src/core/services/overlay-window-bounds.test.ts src/window-trackers/hyprland-tracker.test.ts`
- `bun run typecheck`
- `bun run changelog:lint`
- `bun run test:fast`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,43 @@
---
id: TASK-291
title: Add manual AniList selection for character dictionary resolution
status: Done
assignee:
- '@codex'
created_date: '2026-04-25 21:29'
updated_date: '2026-04-25 22:51'
labels:
- dictionary
- anilist
- cli
- ui
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add CLI and in-app UI support for correcting character dictionary anime resolution when guessit or AniList search picks the wrong series. Manual selections must apply to the whole detected series, persist across episodes, and replace stale incorrect entries in the auto-sync merged character dictionary state. Do not add tray UI. Known regression case: `Re - ZERO, Starting Life in Another World (2016) - S01E01 - - The End of the Beginning and the Beginning of the End [v2 Bluray-1080p Proper][10bit][x265][FLAC 2.0][EN+JA]-SCY.mkv` previously resolved to `10607 - Rerere no Tensai Bakabon`; it should be correctable to the chosen Re:ZERO AniList media and then reused for later files in that series.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 CLI can show AniList candidate matches for the current/target media and set a manual character-dictionary AniList override.
- [x] #2 In-app UI can show the current character-dictionary match, candidate matches, and apply an override without adding tray controls.
- [x] #3 Persisted overrides are keyed at series scope so all later episodes in the same series reuse the selected AniList media.
- [x] #4 Applying an override clears stale guess state, replaces the old incorrect active media entry in auto-sync state, rebuilds/imports the merged character dictionary, and refreshes subtitle dictionary usage.
- [x] #5 Regression tests cover the Re:ZERO filename, override reuse, stale active-media replacement, CLI handling, and IPC/UI contract behavior.
- [x] #6 Docs are updated for manual character dictionary anime selection.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused override/resolution layer for character dictionary media selection: derive a stable series key from filename/guessit data, persist manual AniList media overrides under user data, and expose AniList candidate search helpers.
2. Update character dictionary snapshot resolution to check manual overrides before guessit-derived AniList search, and update auto-sync so applying an override removes stale incorrect media IDs and rebuilds/imports the merged dictionary.
3. Extend CLI with commands to list candidates and set an override for current or target media.
4. Extend existing in-app settings UI via IPC/preload contracts: show current match/candidates and let user apply an override. No tray controls.
5. Use TDD: add failing regressions first for Re:ZERO parsing/override behavior, auto-sync replacement, CLI handling, IPC contract, and UI state; then implement.
6. Update docs-site/manual docs for manual character dictionary anime selection, launcher usage, and the default `Ctrl+Alt+A` modal shortcut, then run focused tests and broader gates as time permits.
<!-- SECTION:PLAN:END -->
@@ -0,0 +1,52 @@
---
id: TASK-292
title: Restore Linux multi-subtitle copy digit capture
status: Done
assignee:
- '@codex'
created_date: '2026-04-25 21:31'
updated_date: '2026-04-25 21:36'
labels:
- bug
- linux
- shortcuts
- clipboard
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
On Linux, the copy-subtitle-multiple shortcut opens the numeric prompt but the follow-up digit is not captured, so the flow times out. User confirmed `wl-copy` itself is installed and working, so investigate the shortcut/digit capture path and restore multi-line subtitle copy without regressing existing session action behavior.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Linux copy-subtitle-multiple shortcut accepts a follow-up digit and copies that number of recent subtitle lines instead of timing out.
- [x] #2 The fix avoids depending on Linux Electron global shortcut digit registration for the follow-up numeric selection when a renderer-visible session can handle it.
- [x] #3 Regression tests cover the Linux multi-copy shortcut/digit flow and existing non-Linux/global shortcut behavior remains intact.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add failing regression coverage for Linux copy-subtitle-multiple local shortcut fallback starting renderer/session numeric selection instead of main-process digit globalShortcut capture.
2. Patch the overlay shortcut fallback/runtime path so Linux visible-overlay multi-copy and mine-sentence-multiple can dispatch session-action numeric selection when renderer handling is available, while preserving main-process numeric sessions for CLI/non-renderer paths.
3. Run targeted tests for shortcut fallback, overlay runtime, and renderer keyboard numeric selection; then run typecheck or a wider focused gate if needed.
4. Update task acceptance criteria/final notes after verification.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented the approved path by keeping multi-step numeric overlay shortcuts out of the main-process local fallback. The visible overlay now receives the original keydown and uses the existing renderer/session-action numeric selection flow for follow-up digits, avoiding Linux Electron globalShortcut digit capture for multi-copy and mine-sentence-multiple. Verification: targeted shortcut/renderer tests and changelog lint pass. `bun run typecheck` is currently blocked by unrelated existing errors in CLI/AniList dictionary-candidate work and `src/main/dependencies.ts` manual-selection API shape.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Restored Linux multi-line subtitle copy by preventing main-process overlay shortcut fallback from consuming multi-step numeric shortcuts (`copySubtitleMultiple` and `mineSentenceMultiple`). Those shortcuts now fall through to the visible overlay renderer, where the existing session binding flow prompts for a digit and dispatches the counted session action locally instead of relying on Electron globalShortcut digit registration. Added regression coverage for the fallback behavior and renderer follow-up digit dispatch, plus a changelog fragment.
Verification: `bun test src/core/services/overlay-shortcut-handler.test.ts src/renderer/handlers/keyboard.test.ts`; `bun run changelog:lint`. Full `bun run typecheck` was attempted but is blocked by unrelated current worktree errors in CLI/AniList dictionary-candidate tests/types and `src/main/dependencies.ts`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,25 @@
---
id: TASK-293
title: Fix interjection tokens receiving subtitle annotations
status: In Progress
assignee: []
created_date: '2026-04-25 22:50'
labels:
- tokenizer
- bug
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Standalone interjections such as あ should remain hoverable dictionary tokens but must not receive N+1, frequency, JLPT, or known-word subtitle annotation metadata.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 A MeCab 感動詞 token like あ is excluded by the shared subtitle annotation gate.
- [ ] #2 annotateTokens strips N+1/frequency/JLPT/known metadata from the interjection while preserving token lookup fields.
- [ ] #3 Focused tokenizer regression passes.
<!-- AC:END -->
@@ -0,0 +1,32 @@
---
id: TASK-294
title: Fix annotated subtitle tokens to honor subtitleStyle
status: Done
assignee: []
created_date: '2026-04-25 23:04'
updated_date: '2026-04-25 23:07'
labels:
- subtitles
- renderer
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Annotated token spans should inherit the configured subtitleStyle typography and only use annotation metadata to change token color.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Tokenized/annotated subtitles preserve configured base subtitle typography such as font family, size, weight, line height, letter spacing, text rendering, and text shadow.
- [x] #2 Known/N+1/JLPT/frequency/name-match annotations affect token color only, plus existing token metadata/hover affordances.
- [x] #3 A renderer regression test covers annotated token rendering with custom subtitleStyle.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated renderer subtitle annotation CSS so known/N+1/name/JLPT/frequency classes no longer override typography with token-specific shadows, underlines, padding, or hover font-weight. Added regression coverage using the user's custom subtitleStyle shape to verify annotated token spans inherit base typography and annotation CSS changes token color only. Verified with `bun test src/renderer/subtitle-render.test.ts`, `bun run typecheck`, and `bun run test:fast`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,66 @@
---
id: TASK-295
title: Add primary subtitle visibility keybinding
status: Done
assignee:
- Codex
created_date: '2026-04-25 23:09'
updated_date: '2026-04-25 23:45'
labels:
- renderer
- keybindings
- subtitles
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add a `v` keybinding that overrides mpv's default `v` subtitle visibility toggle and instead toggles SubMiner's primary subtitle bar visibility on and off. Secondary subtitle hover behavior is out of scope.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Pressing `v` toggles the primary subtitle bar from visible to hidden.
- [x] #2 Pressing `v` again restores the primary subtitle bar visibility.
- [x] #3 The keybinding does not add or change secondary subtitle hover behavior.
- [x] #4 Relevant automated coverage verifies the toggle behavior.
- [x] #5 Pressing `v` in the mpv/plugin keybinding path also toggles the primary subtitle bar visibility instead of mpv native subtitle visibility.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect existing renderer keybinding and subtitle bar visibility code, including current local edits in touched files.
2. Add a focused failing test for `v` toggling primary subtitle bar visibility without changing secondary hover behavior.
3. Implement the minimal renderer/keybinding change.
4. Run targeted tests and update acceptance criteria/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented renderer-local `KeyV` handling before session/mpv binding dispatch so mpv `sub-visibility` is not touched. Visibility state is stored in renderer state and applied via `primary-sub-hidden` class on the primary subtitle container.
Scope updated after user clarified the toggle must work when focus is in mpv as well as in the overlay renderer. Added a forced mpv plugin binding for `v` that runs `--toggle-primary-subtitle-bar`, then broadcasts a renderer IPC toggle event and reuses the same primary subtitle bar toggle path.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added a renderer-local `v` key handler that toggles primary subtitle bar visibility by adding/removing `primary-sub-hidden` on the primary subtitle container.
- Added renderer state for the toggle so repeated presses restore the bar without issuing mpv `sub-visibility` commands.
- Added a forced mpv plugin `v` binding that invokes `--toggle-primary-subtitle-bar` and broadcasts the same renderer toggle event.
- Added CSS for the hidden primary subtitle bar state and regression coverage for both overlay and mpv/plugin entry points.
Tests:
- `bun test src/renderer/handlers/keyboard.test.ts --test-name-pattern "primary subtitle visibility key"`
- `bun test src/cli/args.test.ts --test-name-pattern "session action"`
- `bun test src/core/services/cli-command.test.ts --test-name-pattern "visibility and utility"`
- `bun test src/cli/args.test.ts src/cli/help.test.ts src/core/services/cli-command.test.ts src/main/runtime/cli-command-context.test.ts src/main/runtime/cli-command-context-deps.test.ts src/main/runtime/cli-command-context-main-deps.test.ts src/main/runtime/cli-command-context-factory.test.ts src/main/runtime/composers/cli-startup-composer.test.ts src/main/runtime/first-run-setup-service.test.ts`
- `lua scripts/test-plugin-start-gate.lua && lua scripts/test-plugin-lua-compat.lua`
- `bun run typecheck`
- `bun run test:fast`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,54 @@
---
id: TASK-296
title: Suppress crash notification when closing launcher-managed video
status: Done
assignee: []
created_date: '2026-04-25 23:12'
updated_date: '2026-04-26 02:44'
labels:
- bug
- launcher
- mpv
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Investigate and fix regression where closing a running mpv video causes SubMiner/Electron service crash notification (`<html><tt>/SubMiner</tt> has encountered a fatal error and was closed.</html>`). Not present on origin/main/v0.12.0 path.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Closing a launcher-managed video stops the overlay/app without desktop crash notification.
- [x] #2 Regression test covers the shutdown path that caused the notification.
- [x] #3 Relevant launcher/app tests pass.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Confirm notification source from local crash metadata and mpv/app logs.
2. Add regression coverage for mpv quit/shutdown lifecycle helper spawning.
3. Update mpv Lua lifecycle to avoid Electron helper subprocesses during quit/shutdown while preserving normal end-file hide behavior.
4. Run plugin tests and changelog lint.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Crash records in ~/.cache/drkonqi show SIGBUS in Electron NetworkService utility process for SubMiner AppImage. mpv log shows shutdown starts two `/home/sudacode/.local/bin/SubMiner.AppImage --hide-visible-overlay` subprocesses and kills them during close. Root cause is mpv plugin spawning Electron control helpers during quit/shutdown.
Follow-up after retest: installed plugin matched source patch and no close-time hide command was spawned. New mpv log shows the initial `/home/sudacode/.local/bin/SubMiner.AppImage --start ...` subprocess remains owned by mpv for the whole playback and is killed when mpv quits. New DrKonqi crash at 2026-04-25 16:44 again shows SIGBUS in Electron NetworkService from that AppImage mount. Need detach the long-lived plugin-launched `--start` app process from mpv.
Second fix: plugin-launched `--start` now includes `--background`, using SubMiner's existing background relaunch path so mpv owns only a short-lived starter process rather than the long-running Electron app. Ran `make install-plugin` so ~/.config/mpv/scripts/subminer now contains both lifecycle and background-start fixes. Full `scripts/test-plugin-start-gate.lua` is currently blocked by an unrelated dirty-worktree primary subtitle bar binding test from TASK-297.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Changed the mpv Lua lifecycle so `shutdown` no longer spawns a `--hide-visible-overlay` helper, and `end-file` skips that helper when mpv reports `reason = "quit"`. Also changed plugin-launched `--start` to include `--background`, so mpv owns only SubMiner's short background launcher process instead of the long-running Electron/AppImage process. This addresses both observed crash sources: close-time helper commands and mpv killing the main SubMiner child process at quit. Installed the updated plugin into `~/.config/mpv/scripts/subminer` with `make install-plugin`, and the user confirmed the latest close no longer produced the notification.
Tests: `lua scripts/test-plugin-start-gate.lua` initially proved the shutdown regression failed before the lifecycle fix; full start-gate is currently affected by other dirty work in this file. Passing checks for this commit: `lua scripts/test-plugin-lua-compat.lua && lua scripts/test-plugin-binary-windows.lua`; `bun run changelog:lint`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,32 @@
---
id: TASK-297
title: Fix subtitle annotation color priority after typography cleanup
status: Done
assignee: []
created_date: '2026-04-25 23:44'
updated_date: '2026-04-25 23:46'
labels:
- subtitles
- renderer
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Known-word and frequency subtitle token colors should keep their configured priority after annotation CSS stopped using JLPT underlines.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Known-word token color takes priority over JLPT and frequency color classes.
- [x] #2 Frequency single-mode token color takes priority over JLPT color classes when frequency annotation is active.
- [x] #3 Regression coverage verifies CSS selectors do not allow JLPT color rules to override higher-priority annotation colors.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Scoped JLPT token color selectors so they only apply when no higher-priority known-word, N+1, name-match, or frequency class is present. This keeps known words green and frequency single-mode tokens using the single frequency color instead of being visually overridden by JLPT colors. Added CSS regression assertions for this priority behavior. Verified with `bun test src/renderer/subtitle-render.test.ts`, `bun run typecheck`, and `bun run test:fast`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,54 @@
---
id: TASK-298
title: Exclude kana grammar-helper merges like ことに from subtitle annotations
status: Done
assignee:
- codex
created_date: '2026-04-26 00:08'
updated_date: '2026-04-26 00:15'
labels:
- tokenizer
- annotations
- bug
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Investigate and fix subtitle tokenizer annotation behavior where all-hiragana grammar-helper merged tokens such as `ことに` can be marked as N+1. Current likely path: Yomitan emits `ことに` with headword `こと`; MeCab enrichment supplies content-led POS (`名詞|助詞`, likely `非自立|格助詞`); shared subtitle annotation filter does not exclude this family unless it matches narrower rules such as `これで` or explanatory endings.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `ことに`-style kana grammar-helper merges are not marked known, N+1, JLPT, or frequency-highlighted when their MeCab metadata indicates a non-independent noun plus helper particle.
- [x] #2 Regression coverage demonstrates the reported subtitle phrase does not mark `ことに` as N+1 while preserving annotation for real lexical content tokens.
- [x] #3 Existing tokenizer annotation tests pass.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
Approved approach (user: "let's do it"):
1. Add a regression test for the reported `ことに` case using Yomitan token `ことに` -> headword `こと` and MeCab metadata `名詞|助詞` / `非自立|格助詞`; assert all annotation fields are stripped while nearby lexical content can still be N+1.
2. Verify the new test fails before production changes.
3. Update the shared subtitle annotation filter to exclude conservative kana-only grammar-helper merges: merged surface differs from headword, surface is kana-only, first POS component is `名詞`, first POS2 component is `非自立`, and remaining POS components are grammar helpers (`助詞`/`助動詞`).
4. Run targeted tokenizer/annotation tests and update the task acceptance criteria/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Red test initially passed with headword `こと` because `こと` is already in `JLPT_EXCLUDED_TERMS` and the shared subtitle annotation filter checks that set. Updated regression to the live-risk shape `surface=ことに`, `headword=事`, with MeCab POS `名詞|助詞` / `非自立|格助詞`; this failed before the filter change and passed after.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented a conservative shared subtitle annotation filter for kana-only non-independent noun helper merges. Tokens such as `ことに` with a kanji dictionary headword like `事` are now stripped of known-word, N+1, JLPT, and frequency metadata when MeCab shows the first component as `名詞/非自立` and trailing components as grammar helpers.
Added unit coverage in `src/core/services/tokenizer/annotation-stage.test.ts` and an integration-style tokenizer regression for the reported phrase shape in `src/core/services/tokenizer.test.ts`, verifying `ことに` stays plain while a real lexical token can still become the N+1 target.
Validation: `bun test src/core/services/tokenizer/annotation-stage.test.ts`; `bun test src/core/services/tokenizer.test.ts`; `bun run test:fast`; `bun run changelog:lint`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,66 @@
---
id: TASK-299
title: Force audio replacement during manual subtitle mining
status: Done
assignee:
- Codex
created_date: '2026-04-26 00:10'
updated_date: '2026-04-26 02:42'
labels:
- anki
- mining
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Manual subtitle mining via the Ctrl+C/Ctrl+V flow should replace expression and sentence audio fields even when the user has configured media overwrite fields to false. These fields can already contain proxy-inserted SubMiner audio on a new card, and manual update intent is to replace that generated content.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Manual subtitle mining replaces existing expression audio content regardless of configured audio overwrite settings.
- [x] #2 Manual subtitle mining replaces existing sentence audio content regardless of configured audio overwrite settings.
- [x] #3 Non-manual mining/update flows continue to respect configured audio overwrite settings.
- [x] #4 A regression test covers manual audio replacement when overwrite settings are disabled.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Locate the manual subtitle mining Ctrl+C/Ctrl+V flow and the Anki media field overwrite gate.
2. Add a failing regression test showing manual mining overwrites expression and sentence audio when configured audio overwrite is disabled.
3. Implement the smallest path-specific override so only manual subtitle mining forces audio replacement.
4. Run the focused mining test and update task acceptance criteria/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented focused manual clipboard update behavior in CardCreationService.updateLastAddedFromClipboard: generated manual audio is written to both resolved sentence audio and expression audio fields with forced overwrite. Other update flows still use existing overwrite config paths.
Verification: focused Anki tests passed; typecheck passed; changelog lint and diff check passed. Full bun run test:fast was attempted but is blocked by unrelated existing tokenizer annotation-stage failures tied to dirty task 298 worktree changes.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Manual clipboard subtitle updates now resolve both sentence audio and expression audio fields and replace both with the newly generated audio regardless of ankiConnect.behavior.overwriteAudio.
- Added a regression test for the Ctrl+C/Ctrl+V manual update path with existing proxy-inserted audio and overwriteAudio disabled.
- Registered the regression test in test:fast, documented the overwrite exception in user docs, and added a changelog fragment.
Verification:
- bun test src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/card-creation.test.ts
- bun run tsc --noEmit
- bun test src/main-entry-runtime.test.ts src/anki-integration.test.ts src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/anki-connect-proxy.test.ts src/anki-integration/field-grouping-workflow.test.ts src/anki-integration/field-grouping.test.ts src/anki-integration/field-grouping-merge.test.ts src/release-workflow.test.ts src/prerelease-workflow.test.ts src/ci-workflow.test.ts scripts/build-changelog.test.ts scripts/electron-builder-after-pack.test.ts scripts/mkv-to-readme-video.test.ts scripts/run-coverage-lane.test.ts scripts/update-aur-package.test.ts
- bun run changelog:lint
- bun run docs:test
- bun run docs:build
- git diff --check
Blocked gate:
- bun run test:fast currently fails in unrelated src/core/services/tokenizer/annotation-stage.test.ts tests for kana-only non-independent noun helper merges; those files have pre-existing dirty changes outside this task.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,51 @@
---
id: TASK-300
title: Fix transparent subtitle hover background config
status: Done
assignee: []
created_date: '2026-04-26 03:23'
updated_date: '2026-04-26 03:26'
labels:
- bug
- overlay
- config
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
User reports setting subtitleStyle.hoverTokenBackgroundColor to transparent still renders default hover background in overlay subtitles.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Transparent hoverTokenBackgroundColor is accepted by config resolution.
- [x] #2 Renderer applies transparent hover token background instead of falling back to default.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Reproduce config alias behavior with a failing config test.
2. Map subtitleStyle.hoverBackground to hoverTokenBackgroundColor in config resolution while keeping canonical key precedence.
3. Add renderer regression for transparent hover token background CSS variable.
4. Update docs and changelog fragment; run focused verification.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Local user config used subtitleStyle.hoverBackground, which was ignored because only subtitleStyle.hoverTokenBackgroundColor was recognized. Canonical key still takes precedence when both are present.
Verification passed: bun run test:config:src; bun test src/renderer/subtitle-render.test.ts; bun run changelog:lint; bun run docs:test; bun run docs:build.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented config compatibility for transparent hover token backgrounds. `subtitleStyle.hoverBackground` now maps to the canonical `subtitleStyle.hoverTokenBackgroundColor` during resolution, preserving canonical key precedence. Added regression coverage for the alias and renderer handling of `transparent`, documented the alias, and added a changelog fragment.
Verification: `bun run test:config:src`; `bun test src/renderer/subtitle-render.test.ts`; `bun run changelog:lint`; `bun run docs:test`; `bun run docs:build`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,55 @@
---
id: TASK-301
title: Fix launcher-managed video close leaving background app alive
status: Done
assignee:
- Codex
created_date: '2026-04-26 03:29'
updated_date: '2026-04-26 03:44'
labels:
- bug
- launcher
- mpv
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Launcher/plugin-managed video playback should not leave the SubMiner background app or tray icon running after the video closes unless the user explicitly launched SubMiner in background mode via --background or by starting with no app arguments. This is a regression after crash-avoidance work that added background startup for launcher-managed playback.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Closing a launcher-managed video exits the launcher-started SubMiner app/tray instead of leaving it alive.
- [x] #2 Explicit background launches still keep SubMiner alive after windows close.
- [x] #3 No-argument app startup behavior remains unchanged.
- [x] #4 Regression coverage exercises the launcher-managed playback shutdown lifecycle.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add regression coverage first: plugin auto-start should tag launcher-managed playback, and app mpv shutdown handling should quit only when started in that managed playback mode.
2. Add a narrow CLI flag/state field for launcher-managed playback, separate from explicit persistent background mode.
3. Have plugin pass the new flag with its background start command.
4. On mpv shutdown/disconnect, request app quit only when managed playback mode is active; preserve explicit --background and no-arg startup persistence.
5. Run focused plugin/app tests, then relevant launcher/core gates if feasible.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented managed playback shutdown by adding a `--managed-playback` app flag that the mpv plugin passes only for launcher-managed starts. The main mpv shutdown path now quits the app when initial args indicate managed playback, while explicit background/no-arg startup remains persistent. Added plugin start-gate and mpv protocol regression coverage.
Implemented managed playback lifecycle: mpv plugin auto-start passes --background --managed-playback; app quits on mpv shutdown only when initial args include managedPlayback. Explicit --background and no-arg startup remain persistent. Installed updated mpv plugin to ~/.config/mpv/scripts/subminer via make install-plugin.
Retest showed tray still remained. Root cause: relying on mpv's JSON IPC shutdown event was insufficient; the app may only see the socket close. Added managed-playback quit on MpvIpcClient onClose before reconnect scheduling, with regression coverage.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Launcher-managed playback now starts SubMiner with an internal --managed-playback marker alongside --background. The app requests quit either when mpv sends shutdown or when the mpv IPC socket closes, but only for managed playback mode; explicit background/no-arg startup remains persistent. Added CLI, mpv protocol, mpv socket-close, and plugin regression coverage plus a launcher changelog fragment. Rebuilt the app/launcher and confirmed focused checks, typecheck, build, plugin tests, dist smoke, and formatting.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,53 @@
---
id: TASK-302
title: Add visible hover affordance for annotated subtitle tokens
status: Done
assignee: []
created_date: '2026-04-26 03:39'
updated_date: '2026-04-26 03:40'
labels:
- overlay
- subtitle
- ux
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Annotated subtitle tokens keep annotation colors on hover, but with transparent hover backgrounds there is no visible hover indication. Add a subtle affordance that preserves annotation color semantics.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Annotated token hover keeps annotation color instead of switching to hover text color.
- [x] #2 Annotated token hover has a visible indication when hover background is transparent.
- [x] #3 Regression tests cover the hover CSS contract.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add failing CSS contract test for annotated token hover affordance.
2. Add brightness/saturation filter to annotated token hover CSS without changing annotation color.
3. Add changelog fragment and run focused verification.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented option 1 from approved design: annotated subtitle word hover keeps annotation color and adds `filter: brightness(1.18) saturate(1.08)` for visible affordance when the hover background is transparent.
Verification passed: `bun test src/renderer/subtitle-render.test.ts`; `bun run changelog:lint`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added a visible hover affordance for annotated subtitle tokens without changing annotation color semantics. Annotated word hover now applies a small brightness/saturation lift while retaining the existing background behavior, so transparent hover backgrounds still show feedback.
Regression coverage updated in `src/renderer/subtitle-render.test.ts` to assert the hover filter is present and that hover color overrides are still absent for annotated tokens. Added changelog fragment `changes/302-annotated-hover-affordance.md`.
Verification: `bun test src/renderer/subtitle-render.test.ts`; `bun run changelog:lint`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,57 @@
---
id: TASK-303
title: Update tray menu help action
status: Done
assignee:
- Codex
created_date: '2026-04-26 03:54'
updated_date: '2026-04-26 04:12'
labels:
- tray
- overlay
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Replace the tray menu's direct visible-overlay open action with an action that opens the existing in-session help modal. The tray should no longer expose an "Open Overlay" menu item; users should be able to open help from the tray instead.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Tray menu no longer includes an "Open Overlay" option.
- [x] #2 Tray menu includes an option to open the session help modal.
- [x] #3 Selecting the new tray help option initializes overlay runtime if needed and invokes the existing session help modal path.
- [x] #4 Focused regression tests cover the menu label and action wiring.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused regression coverage for tray menu template labels and main-process action wiring: assert Open Overlay is absent, Open Help is present, and clicking help initializes overlay runtime if needed before opening the existing session help modal path.
2. Update tray runtime action types/template to replace openOverlay with openSessionHelp.
3. Update tray main action builder dependencies to call the existing openSessionHelpModal function after overlay runtime initialization.
4. Run targeted tray tests, then broader relevant fast tests if needed.
5. Check acceptance criteria and finalize backlog notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented tray menu replacement via existing session help overlay path. Verification passed: targeted tray tests (`bun test src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts`), SubMiner verifier lanes `runtime-compat` and `docs`, and `bun run changelog:lint`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Replaced the tray menu's `Open Overlay` item with `Open Help`, wired it to initialize overlay runtime when needed, and then open the existing session help modal path. Updated tray runtime/main-deps/action tests to assert the old label is absent, the new label is present, and the new action calls the help modal. Added changelog fragment `changes/303-tray-help-menu.md`.
Verification:
- `bun test src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts src/main/runtime/tray-main-deps.test.ts src/main/runtime/tray-runtime-handlers.test.ts`
- `bash plugins/subminer-workflow/skills/subminer-change-verification/scripts/verify_subminer_change.sh --lane runtime-compat --lane docs src/main/runtime/tray-runtime.ts src/main/runtime/tray-main-actions.ts src/main/runtime/tray-main-deps.ts src/main.ts changes/303-tray-help-menu.md`
- `bun run changelog:lint`
Verifier artifacts: `.tmp/skill-verification/subminer-verify-20260425-211156-9fkdDf/`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,27 @@
---
id: TASK-304
title: Fix N+1 sentence boundary counting across Yomitan punctuation gaps
status: In Progress
assignee: []
created_date: '2026-04-26 05:33'
labels:
- bug
- tokenizer
- annotations
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
N+1 target selection should respect sentence-ending punctuation from the original subtitle text even when Yomitan token output omits punctuation tokens. Current behavior can treat multiple subtitle sentences as one token span and incorrectly satisfy the minimum content-token threshold.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 A subtitle like `てんめ!ふざけんなよ!` does not mark `ふざけん`/similar single-content-token second sentence as N+1 when the minimum sentence word count is 3.
- [ ] #2 N+1 sentence segmentation uses original subtitle text offsets or equivalent source-boundary data, not only punctuation tokens returned by Yomitan.
- [ ] #3 Existing annotation exclusion behavior for particles/grammar tokens remains unchanged.
- [ ] #4 Regression tests cover Yomitan-style token streams where punctuation is absent from the token list.
<!-- AC:END -->
@@ -0,0 +1,64 @@
---
id: TASK-305
title: Use Yomitan word classes for subtitle token POS filtering
status: Done
assignee:
- Codex
created_date: '2026-04-26 05:56'
updated_date: '2026-05-02 22:47'
labels:
- tokenizer
- yomitan
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Subtitle annotation filtering currently uses Yomitan token spans, then enriches those spans by running MeCab over the full normalized subtitle line. Add support for carrying Yomitan headword wordClasses from termsFind into SubMiner tokens so dictionary-backed tokens can provide coarse POS/tag metadata without vendored Yomitan changes. MeCab whole-line enrichment should remain a fallback/source of detailed POS data when Yomitan classes are absent.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Yomitan scanner tokens preserve matched headword wordClasses when termsFind returns them.
- [x] #2 Subtitle tokenization maps recognized Yomitan wordClasses to coarse PartOfSpeech/POS metadata before annotation filtering.
- [x] #3 Whole-line MeCab enrichment remains available for missing or more detailed POS metadata and does not break existing subtitle annotation behavior.
- [x] #4 Focused tokenizer tests cover wordClasses extraction and POS mapping.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused regression coverage for Yomitan scanner wordClasses payload and subtitle POS mapping.
2. Extend the app-owned Yomitan scanner payload to carry matched headword wordClasses when present.
3. Map recognized Yomitan wordClasses to SubMiner coarse PartOfSpeech/POS metadata before annotation filtering.
4. Keep MeCab whole-line enrichment as fallback/detail-fill for missing POS fields.
5. Run focused tokenizer tests and typecheck.
2026-05-02 review follow-up: inspect latest CodeRabbit review on PR #57, classify each finding as actionable/not actionable, patch scoped issues, run focused verification, then update final notes. User request to address/assess the review is the approval for this follow-up.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented app-only wordClasses extraction from termsFind results; no vendored Yomitan changes required. Recognized classes currently map prt, aux, v*, adj-i/adj-ix, adj-na, and noun-like classes to SubMiner POS metadata. MeCab enrichment now skips only tokens with complete pos1/pos2/pos3 and otherwise fills missing fields while preserving existing coarse pos1. Verification: bun test src/core/services/tokenizer/yomitan-parser-runtime.test.ts src/core/services/tokenizer.test.ts; bun run typecheck.
2026-05-02 CodeRabbit latest review assessment: only current actionable finding was in src/core/services/tokenizer/annotation-stage.test.ts, where a kana-only regression fixture used mixed-script/punctuation surface text. Earlier CodeRabbit findings in this PR were already marked addressed by prior commits. Patched the fixture to use pure-kana surface/headword and renamed the test to match the exercised behavior. Verification: bun test src/core/services/tokenizer/annotation-stage.test.ts; bun run typecheck.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented app-only Yomitan wordClasses support for subtitle token annotation filtering. The scanner carries matched headword wordClasses from termsFind results, tokenizer maps recognized classes into SubMiner coarse POS metadata before annotation, and MeCab whole-line enrichment continues to fill missing detailed POS fields without requiring vendored Yomitan changes.
2026-05-02 CodeRabbit follow-up:
- Assessed the latest CodeRabbit review on PR #57. Only one new actionable finding remained: the kana-only N+1 regression test used a mixed/punctuated surface.
- Updated the fixture in src/core/services/tokenizer/annotation-stage.test.ts to use a pure-kana unknown target and renamed the test accordingly.
Tests run:
- bun test src/core/services/tokenizer/annotation-stage.test.ts
- bun run typecheck
Note: earlier CodeRabbit findings on this PR were already marked addressed in prior commits; no further latest-review issues were left unresolved in this pass.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,33 @@
---
id: TASK-306
title: Fix Hyprland fullscreen overlay geometry and hover pause
status: Done
assignee: []
created_date: '2026-04-27 01:44'
labels:
- linux
- hyprland
- overlay
- bug
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Overlay should track mpv geometry through Hyprland fullscreen transitions, stay above fullscreen video, and keep primary subtitle hover pause working after fullscreen/toggle cycles.
Implemented by observing mpv fullscreen property changes in addition to Hyprland geometry events, then refreshing visible overlay bounds/layering on Linux.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Hyprland tracker reacts to fullscreen/window state changes with updated geometry.
- [x] #2 Visible overlay is re-layered above mpv after Hyprland fullscreen geometry updates.
- [x] #3 Primary subtitle hover pause remains active after overlay geometry changes or visible overlay toggle cycles.
<!-- AC:END -->
@@ -0,0 +1,54 @@
---
id: TASK-306
title: Separate background stats daemon from regular SubMiner app
status: Done
assignee: []
created_date: '2026-04-27 00:56'
updated_date: '2026-04-27 01:00'
labels:
- stats
- runtime
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Background stats mode should run only the stats data/server pieces. It must not bring up tray UI or expose the regular mpv connection surface, and stopping should remain CLI-only.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launching stats background mode starts a separate stats daemon process rather than booting the regular SubMiner runtime.
- [x] #2 Background stats mode does not create or keep a tray icon.
- [x] #3 Background stats mode does not start mpv IPC/client surfaces that let mpv connect to the app.
- [x] #4 Background stats mode remains stoppable through the stats stop command line path.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add entry-runtime tests for public stats background/stop daemon detection.
2. Implement early public stats daemon command detection and route it before regular app boot.
3. Run targeted tests and update task status/criteria.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented early public stats daemon routing in main-entry runtime. Direct `--stats-background` and `--stats-stop` now resolve to daemon control before single-instance lock and before loading `main.js`, matching the existing internal launcher daemon flags. Installed missing Bun dependencies to run targeted tests.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added `resolveStatsDaemonCommandAction` and updated entry detection so public `--stats-background` / `--stats-stop` invocations route through the isolated stats daemon control path.
- Reused that action resolution in `stats-daemon-entry` so public stop commands map to stop instead of the default start path.
- Added regression coverage for public daemon detection/action resolution.
Verification:
- `bun test src/main-entry-runtime.test.ts launcher/commands/command-modules.test.ts src/main/runtime/stats-cli-command.test.ts src/stats-daemon-control.test.ts`
- `bun run typecheck`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-307
title: Defer in-app stats server to running background stats daemon
status: Done
assignee: []
created_date: '2026-04-27 01:57'
updated_date: '2026-04-27 02:02'
labels:
- stats
- runtime
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When normal SubMiner app startup has stats auto-start enabled, it should detect an already-running background stats daemon and avoid starting a second in-app stats server. Stats overlay/dashboard URL resolution should point at the background daemon.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 If a live background stats daemon state exists for another process, in-app stats auto-start does not start a local stats server.
- [x] #2 Stats URL resolution returns the background daemon URL when the background daemon is live.
- [x] #3 Stale or dead background daemon state is cleared and normal in-app stats startup still works.
- [x] #4 Regression tests cover the deferral behavior.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add unit tests for stats server routing decisions around live/stale background daemon state.
2. Implement a small routing helper used by main stats startup.
3. Wire `ensureStatsServerStarted()` through the helper.
4. Run targeted tests/typecheck/changelog lint and finalize the task.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Extracted stats server URL routing into `src/main/runtime/stats-server-routing.ts` and wired `main.ts` through it. The helper returns the background daemon URL without calling local server startup when a live external daemon exists; dead/self-owned stale state is removed before falling back to local startup. Added the new test to `test:core:src`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added a pure stats server routing helper that chooses between a live background daemon and local in-app stats server startup.
- Updated main stats URL resolution to defer to another process's background daemon and only start the in-app server when no live daemon is available.
- Added regression tests for live daemon deferral, dead daemon cleanup, self-owned stale state cleanup, and local server reuse.
- Added the routing test to the core source test lane and added a changelog fragment.
Verification:
- `bun test src/main/runtime/stats-server-routing.test.ts src/main-entry-runtime.test.ts src/main/runtime/stats-cli-command.test.ts src/stats-daemon-control.test.ts`
- `bun run test:core:src`
- `bun run typecheck`
- `bun run changelog:lint`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-307
title: Exclude kana-only words from N+1 subtitle targets
status: Done
assignee:
- codex
created_date: '2026-04-27 01:52'
updated_date: '2026-04-27 01:57'
labels:
- tokenizer
- annotations
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Subtitle N+1 annotation is over-targeting kana-only or hiragana/katakana tokens that collapse to dictionary words. Adjust targeting so kana-only tokens are not selected as N+1 candidates, while preserving tokenization/hover behavior and other annotation metadata where existing filters allow it.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Kana-only subtitle tokens are not marked as N+1 targets.
- [x] #2 Kanji or mixed lexical tokens can still be marked as N+1 targets when they are the single unknown candidate in a sentence.
- [x] #3 Regression coverage demonstrates the kana-only N+1 exclusion.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a failing regression in `src/core/services/tokenizer.test.ts` showing a kana-only Yomitan token is not selected as the single N+1 target, while a mixed lexical token in the same style still can be targeted.
2. Implement the smallest filter in `src/token-merger.ts`: N+1 candidate selection rejects tokens whose surface is entirely kana; word-count behavior remains governed by existing annotation/POS filters.
3. Run the focused tokenizer tests, then update task acceptance criteria/final summary.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented a surface-level kana-only guard in N+1 candidate selection. Kept existing word-count/POS filtering behavior intact; updated tokenizer and annotation-stage expectations where old tests intentionally allowed kana-only N+1 targets.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added kana-only surface detection to `isNPlusOneCandidateToken` so hiragana/katakana-only subtitle tokens are not selected as N+1 targets.
- Added/updated tokenizer and annotation-stage regressions for kana-only targets while preserving non-kana N+1 behavior.
- Added changelog fragment `changes/307-kana-nplusone-targets.md`.
Verification:
- `bun test src/core/services/tokenizer.test.ts --test-name-pattern "kana-only N\+1"` failed before the fix with `true !== false`.
- `bun test src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer.test.ts` passed.
- `bun run typecheck` passed.
- `bun run test:fast` passed.
- `bun run changelog:lint` passed.
- `bunx prettier --check src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/token-merger.ts changes/307-kana-nplusone-targets.md` passed.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,54 @@
---
id: TASK-308
title: Restore persistent JLPT subtitle underlines
status: Done
assignee:
- Codex
created_date: '2026-04-27 02:03'
updated_date: '2026-04-27 02:07'
labels:
- overlay
- jlpt
- renderer
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
JLPT tagging currently exposes the JLPT level on hover, but the persistent subtitle underline is missing. When JLPT annotation is enabled and a rendered subtitle token has a JLPT level, users should see the configured JLPT color underline without needing to hover.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 JLPT-tagged subtitle tokens render a persistent underline for N1-N5 levels when JLPT tagging is enabled.
- [x] #2 Hover and keyboard-selected JLPT labels continue to appear for tagged tokens.
- [x] #3 Higher-priority annotation colors such as known words, N+1, names, and frequency styling are not overridden by JLPT text color.
- [x] #4 Regression coverage verifies the CSS contract for persistent JLPT underlines.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused renderer CSS regression asserting each `word-jlpt-n*` class provides persistent underline decoration while preserving existing typography constraints.
2. Run the focused renderer test to confirm the regression fails before production changes.
3. Restore underline CSS for JLPT classes without broadening JLPT text-color precedence over known/N+1/name/frequency tokens.
4. Re-run the focused renderer test and update acceptance criteria/task notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Verified red/green regression: tightened `src/renderer/subtitle-render.test.ts` first failed because base `word-jlpt-n*` selectors had no underline decoration, then passed after moving JLPT underline decoration to unconditional base selectors while leaving JLPT text color priority-scoped.
Checks: `bun test src/renderer/subtitle-render.test.ts`; `bun run changelog:lint`; `bun run typecheck`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Restored persistent JLPT subtitle underlines by adding underline decoration to each base `word-jlpt-n*` renderer CSS class. JLPT text color remains in the existing priority-scoped selectors, so known/N+1/name/frequency coloring is not overridden while the underline still appears on any JLPT-tagged token.
Updated renderer CSS regression coverage to assert underline decoration for N1-N5 and added a fixed changelog fragment. Verified with `bun test src/renderer/subtitle-render.test.ts`, `bun run changelog:lint`, and `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,57 @@
---
id: TASK-309
title: Accept modified follow-up digits for multi-line sentence mining
status: Done
assignee:
- '@codex'
created_date: '2026-04-27 20:06'
updated_date: '2026-04-27 20:15'
labels:
- bug
- linux
- shortcuts
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
On Linux, `Ctrl+Shift+S` starts multi-line sentence-card mining, but the follow-up digit is not accepted and the prompt times out. Restore reliable digit capture for the multi-mine flow, including the common case where the original shortcut modifiers are still held briefly while pressing the digit.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `Ctrl+Shift+S` followed by a number-row digit creates a counted `mineSentenceMultiple` request instead of timing out.
- [x] #2 Follow-up digit capture works when the user has not fully released `Ctrl`/`Shift` after the starter shortcut.
- [x] #3 Regression coverage includes renderer session bindings and mpv plugin numeric selection.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Backlog MCP unavailable in this session, so this task is tracked via repo-local backlog files.
Implemented renderer digit extraction from `KeyboardEvent.code` for pending numeric selection, so shifted number-row events such as `Ctrl+Shift+Digit3` still dispatch count `3`. Updated the mpv plugin session-binding numeric selector to register bare digits plus the starter shortcut modifier combinations, so plugin-owned `Ctrl+Shift+S` can accept a follow-up digit before the modifiers are fully released.
Verification:
- `bun test src/renderer/handlers/keyboard.test.ts src/core/services/overlay-shortcut-handler.test.ts src/core/services/overlay-window.test.ts`
- `bun run test:plugin:src`
- `bun run changelog:lint`
- `bun x prettier --check src/renderer/handlers/keyboard.ts src/renderer/handlers/keyboard.test.ts package.json 'changes/309-multi-mine-modified-digits.md' 'backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md'`
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Restored multi-line sentence-card digit capture for the case where `Ctrl`/`Shift` are still held after `Ctrl+Shift+S`. The renderer now accepts digits by physical `Digit1`-`Digit9`/`Numpad1`-`Numpad9` code during pending numeric selection, and the mpv plugin registers the matching modified digit bindings for session-binding numeric prompts.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-310
title: Suppress N+1 highlight for kana-only candidate sentences
status: Done
assignee:
- Codex
created_date: '2026-04-28 06:55'
updated_date: '2026-04-28 07:04'
labels:
- tokenizer
- n+1
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Reduce noisy N+1 subtitle annotations when the only unknown candidates in a sentence are kana-only hiragana or katakana words, such as mostly-kana subtitle lines where highlighting a particle/helper-like token is low value.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 N+1 annotation does not mark a kana-only unknown target when all N+1 candidates in the sentence are kana-only.
- [x] #2 N+1 annotation continues to mark kanji or mixed-script unknown targets in otherwise eligible sentences.
- [x] #3 A focused regression test covers the kana-only candidate case.
- [x] #4 N+1 minimum sentence word count excludes tokens stripped by the subtitle annotation filter, so filtered grammar/noise tokens cannot satisfy minSentenceWords.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Keep the existing N+1 target eligibility guard: kana-only subtitle surfaces do not become N+1 targets.
2. Add a focused regression in src/core/services/tokenizer/annotation-stage.test.ts proving annotation-filtered tokens do not count toward ankiConnect.nPlusOne.minSentenceWords.
3. Verify the new regression fails before code changes.
4. Patch src/token-merger.ts so the N+1 minimum sentence word count uses the same subtitle-annotation eligibility filter as annotation rendering, excluding filtered particles/auxiliaries/noise from the count.
5. Re-run focused tokenizer tests, then update TASK-310 acceptance criteria and final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Initial context: current token-merger has an existing surface-level kana-only guard in isNPlusOneCandidateToken, added in commit 9e4ad907. Need decide whether to broaden behavior to lookup/headword forms or verify current behavior only.
Implemented by treating kana-only N+1 candidates as kana-only even when their token surface includes surrounding subtitle punctuation such as ellipsis or dashes. Focused regression was red before the token-merger change: スイッチ… was marked true, then passed after the guard update. test:env initially hit an unrelated immersion-tracker active_days timing/order failure and Bun follow-on loader error; the failing test passed in isolation and the full test:env rerun passed.
Reopened for follow-up scope: minSentenceWords must count annotation-eligible tokens only, not tokens stripped from annotation metadata.
Implemented follow-up minSentenceWords behavior: unknown tokens filtered from N+1 targeting no longer contribute to sentence length; known eligible tokens and true N+1 candidates still count.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Changed N+1 sentence-length counting so minSentenceWords only counts known eligible words and actual N+1 target candidates. Unknown tokens filtered from N+1 targeting, including kana-only unknowns, no longer pad a sentence into eligibility. Existing annotation-filtered particles/auxiliaries remain excluded. Added regression coverage for the filtered unknown padding case while preserving kanji/mixed-script target behavior.
Verification: new regression failed before implementation; `bun test src/core/services/tokenizer/annotation-stage.test.ts -t "N\\+1"` pass; full `bun test src/core/services/tokenizer/annotation-stage.test.ts` pass; `bun test src/core/services/tokenizer.test.ts -t "N\\+1"` pass; `bun run typecheck` pass.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,43 @@
---
id: TASK-311
title: Suppress auxiliary inflection fragments from subtitle annotations
status: Done
assignee: []
created_date: '2026-05-02 09:07'
updated_date: '2026-05-02 09:10'
labels:
- tokenizer
- annotations
- bug
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Suppress standalone Japanese auxiliary/inflection subtitle fragments such as `れる` and `れた` from frequency/JLPT/N+1/known annotation styling while keeping lexical verbs such as `くれ` / `くれる` annotatable. Tokens must remain hoverable; only annotation metadata should be stripped.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `れる` and `れた`-style standalone helper fragments render as plain hoverable subtitle tokens.
- [x] #2 Lexical verbs like `くれ` / `くれる` remain eligible for annotation.
- [x] #3 Regression tests cover unit filter behavior and tokenizer integration.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented with TDD. Added failing coverage first for standalone `れる`/`れた` auxiliary fragments and a lexical `くれ`/`くれる` guard. Updated the shared subtitle annotation filter to strip annotation metadata for kana-only auxiliary inflection fragments identified by MeCab POS (`助動詞` only, or `動詞/接尾` with optional trailing `助動詞`) while preserving lexical `くれ` as `くれる` when tagged `動詞/自立`. Added tokenizer integration coverage for `れた` and neighboring lexical N+1 behavior.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Suppressed annotation metadata for standalone auxiliary inflection fragments such as `れる` and `れた` in subtitle tokens, leaving them hoverable but plain. Preserved lexical `くれ` -> `くれる` verb metadata when MeCab tags it as `動詞/自立`.
Added unit and tokenizer regression coverage, plus a release fragment in `changes/311-auxiliary-inflection-annotation-filter.md`.
Validation: targeted annotation/tokenizer tests passed; `bun run typecheck` passed; `bun run changelog:lint` passed. `bun run test:fast` was attempted twice and failed in unrelated `src/core/services/subsync.test.ts` cross-file state (`window.electronAPI` undefined), while `bun test src/core/services/subsync.test.ts` passes by itself.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,42 @@
---
id: TASK-312
title: Suppress ja-nai explanatory ending subtitle annotations
status: Done
assignee: []
created_date: '2026-05-02 09:55'
updated_date: '2026-05-02 10:03'
labels:
- tokenizer
- annotations
- bug
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Suppress subtitle annotation styling for grammar-only explanatory endings like `じゃない` and `じゃないですか` while preserving nearby lexical content annotations.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `じゃない` and `じゃないですか`-style endings render as plain hoverable subtitle tokens.
- [x] #2 The reported phrase `みたいなのあるじゃないですか` does not annotate `じゃない`/`じゃないですか` as lexical/frequency content.
- [x] #3 Regression tests cover unit filter behavior and tokenizer integration without suppressing lexical content tokens.
- [x] #4 Standalone polite copula endings such as `です` / `ですよ` render as plain hoverable subtitle tokens even if POS metadata is missing or too lexical.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Added failing coverage first for `じゃない` / `じゃないですか` and `ですよ` leaking annotation metadata when POS metadata is missing or too lexical. Implemented term-family exclusions in the shared subtitle annotation filter for the `じゃない` explanatory family and polite copula suffix endings (`ですか`, `ですね`, `ですよ`, `ですな`). Kept bare `です` term-only behavior unchanged to preserve existing no-POS frequency tests; POS-tagged `です` is already stripped by the grammar POS exclusion path.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Suppressed subtitle annotation metadata for grammar-only endings like `じゃないですか` and `ですよ`, while preserving nearby lexical content annotations. Added unit and tokenizer regression coverage for the reported `みたいなのあるじゃないですか` and `感じですよ` shapes, plus changelog fragment `changes/312-grammar-ending-annotation-filter.md`.
Validation: `bun test src/core/services/tokenizer/annotation-stage.test.ts`; `bun test src/core/services/tokenizer.test.ts`; `bun run typecheck`; `bun run changelog:lint`; `git diff --check`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,28 @@
---
id: TASK-313
title: Fix mpv buffering reload overlay lifecycle
status: To Do
assignee: []
created_date: '2026-05-02 22:12'
labels:
- bug
- mpv
- overlay
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
macOS local playback can emit an mpv reload/end-file/file-loaded sequence during buffering. SubMiner should treat same-media reload churn as a continuation, not a fresh playback session, so the visible overlay remains available and startup-only tokenization/AniSkip work is not repeated unnecessarily.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Same-media mpv reload buffering does not hide the visible overlay.
- [ ] #2 Same-media mpv reload buffering does not re-arm the pause-until-ready startup gate or wait for a second tokenization-ready signal.
- [ ] #3 Same-media mpv reload buffering does not repeat AniSkip lookup work for the already-loaded media.
- [ ] #4 Normal new-file playback still clears per-media state, applies managed subtitle defaults, auto-starts/updates the overlay, and runs needed startup checks.
- [ ] #5 Regression coverage exercises the buffering reload/end-file/file-loaded sequence in the mpv plugin lifecycle.
<!-- AC:END -->
@@ -0,0 +1,67 @@
---
id: TASK-315
title: Suppress annotations for standalone じゃない and です ending tokens
status: Done
assignee:
- codex
created_date: '2026-05-03 00:02'
updated_date: '2026-05-03 06:05'
labels:
- bug
- tokenizer
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Standalone `じゃない` grammar ending tokens should not display or persist subtitle annotations even if a dictionary assigns a rank or JLPT/known match. User observed `じゃない` still being marked frequent in overlay after tokenization produced it as a dictionary word.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `じゃない` and `です` ending tokens have known-word, N+1, frequency, and JLPT annotation metadata cleared in subtitle annotation output.
- [x] #2 Common polite/question variants such as `じゃないですか` and `ですよ` remain excluded when tokenized as a single ending token.
- [x] #3 Regression coverage proves same-line Yomitan segments split content from trailing grammar endings so the content word can be annotated without coloring the ending.
- [x] #4 Auxiliary-only helper spans such as `てく` + `れた` in `ベアトリスがいてくれたから` have known-word, N+1, frequency, and JLPT annotation metadata cleared.
- [x] #5 Hard-coded grammar-ending phrase permutations are replaced by shared pattern matching, with parser selection and subtitle annotation filtering using the same grammar-ending classifier.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression for `ベアトリスがいてくれたから` where Yomitan tokens include auxiliary-only `てく` and `れた` with pre-ranked/known/JLPT metadata candidates.
2. Run the targeted test to verify the regression fails before production changes.
3. Patch the shared subtitle annotation filter so kana-only auxiliary helper spans made only of grammar POS components are excluded while preserving lexical content tokens.
4. Re-run targeted tokenizer/annotation tests, then run SubMiner change verification classifier/verifier for the touched files.
5. Update TASK-315 acceptance criteria, notes, and final summary with commands and outcomes.
Replace explicit standalone grammar-ending permutations with a compact shared matcher used by parser selection and annotation filtering.
Add regression tests first for non-enumerated polite copula / ja-nai variants so the matcher behavior is proven, then refactor implementation and verify targeted lanes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented as one focused tokenizer fix. Parser selection now splits dictionary-backed same-line grammar ending segments (`です`, `じゃない*`) from preceding content so annotation styling can apply only to the content token. Shared subtitle annotation filtering now treats bare `です` like the existing `ですか/ですよ/...` copula endings.
2026-05-03: Reopened for approved add-on covering auxiliary-only `てく` + `れた` helper highlighting report.
2026-05-03: Added regression coverage for `ベアトリスがいてくれたから` where Yomitan emits `てく` + `れた` and MeCab enrichment tags `てく` as `助詞|動詞` / `接続助詞|非自立`. The regression initially failed because `てく` kept `isKnown: true` and `jlptLevel: N4`. Added a shared-filter helper for kana-only particle+non-independent-verb helper spans, preserving lexical `自立` verbs. Verification: `bun test src/core/services/tokenizer/annotation-stage.test.ts`, `bun test src/core/services/tokenizer.test.ts`, `bun test src/core/services/tokenizer/parser-selection-stage.test.ts`, `bun x prettier --check ...`, and `bun run typecheck` passed. SubMiner verifier core lane passed typecheck but `bun run test:fast` failed on unrelated existing cross-suite issues: `window.electronAPI` undefined in `src/renderer/handlers/keyboard.ts` during `src/core/services/subsync.test.ts`, followed by Bun `node:test` nested-test cascade.
2026-05-03: Reopened for follow-up requested by user: remove hard-coded standalone grammar-ending permutation list and lean on pattern/POS filtering where possible.
2026-05-03: Added shared `grammar-ending.ts` matcher for polite copula, negative copula, and explanatory endings. Parser selection now uses the standalone-ending matcher instead of `STANDALONE_GRAMMAR_ENDINGS`. Shared subtitle filter now uses the same grammar classifier instead of generated phrase sets. Removed stale duplicate subtitle-exclusion helpers from `annotation-stage.ts`; annotation-stage continues to delegate subtitle exclusion to the shared filter. Verification passed: targeted tokenizer/parser/annotation tests, Prettier check, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`. `bun run changelog:lint` remains blocked by pre-existing malformed fragment `changes/319-interjection-annotation-filter.md`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Replaced grammar-ending phrase permutations with shared pattern matching. `parser-selection-stage.ts` now splits standalone grammar endings through `grammar-ending.ts` instead of `STANDALONE_GRAMMAR_ENDINGS`; `subtitle-annotation-filter.ts` uses the same classifier for polite copula, negative copula, and explanatory endings instead of generated exact phrase sets.
Kept exclusion ownership cleaner: subtitle annotation exclusion remains in the shared filter, while `annotation-stage.ts` no longer carries stale duplicate subtitle-exclusion constants/helpers. Added regressions for pattern coverage including `ではないですか` splitting and no-POS grammar-ending annotation clearing.
Verification passed: targeted tokenizer/parser/annotation tests, Prettier check, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`. `bun run changelog:lint` is blocked by pre-existing malformed `changes/319-interjection-annotation-filter.md`; new fragment `changes/321-grammar-ending-pattern-filter.md` uses the current metadata format.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,68 @@
---
id: TASK-316
title: Fix macOS launcher playback exit with background stats daemon
status: Done
assignee:
- '@Codex'
created_date: '2026-05-03 00:32'
updated_date: '2026-05-03 00:36'
labels:
- bug
- macos
- mpv
- stats
- runtime
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Launching a video on macOS when SubMiner is not already running should not leave the regular SubMiner app/tray alive after mpv closes. A separately running background stats daemon must remain non-blocking and must not be used as a foreground app dependency during playback startup/shutdown.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Closing a launcher/plugin-managed mpv session exits the launcher-started regular SubMiner app/tray after mpv closes.
- [x] #2 Explicit background/no-argument app launches still remain alive as before.
- [x] #3 A live background stats daemon is ignored by normal in-app stats server routing during regular app startup/playback, so the regular app never depends on or connects to that background daemon.
- [x] #4 Regression coverage demonstrates the managed playback shutdown and stats-daemon isolation behavior.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add failing regressions first: stats routing should ignore a live foreign background daemon for normal app URL/server startup, and managed playback disconnect should request app quit directly without reconnecting or depending on overlay/youtube disconnect guards.
2. Implement the narrow runtime changes in `src/main/runtime/stats-server-routing.ts` and, if needed, mpv disconnect plumbing in `src/core/services/mpv.ts` / event deps.
3. Preserve explicit persistent background/no-arg behavior by keeping `--managed-playback` as the only playback-exit marker.
4. Run focused tests (`stats-server-routing`, mpv client/protocol/event tests), then typecheck if focused checks pass.
5. Update changelog and task acceptance/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented regular app stats routing isolation from live background daemon state and explicit managed-playback quit-on-disconnect wiring in main mpv event deps. Existing `MpvIpcClient` socket-close managed playback quit path remains covered.
`bun run test:fast` was attempted after focused verification. It failed in the broad `test:core:src` lane with Bun/node:test nested-test runner errors across many unrelated files and one transient subsync renderer API failure; rerunning the concrete subsync failure alone passed. Focused runtime tests, typecheck, and changelog lint remain green.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Regular app stats server routing no longer returns or depends on a live background daemon URL; it validates/cleans state, then uses the local app stats server path.
- Managed playback is now explicitly treated as a quit-on-disconnect launch mode in main mpv event deps, in addition to the existing mpv socket-close quit request.
- Added regressions for background daemon isolation and managed playback quit-on-disconnect classification.
- Added changelog fragment `changes/316-macos-playback-stats-daemon.md`.
Verification:
- `bun test src/main/runtime/stats-server-routing.test.ts src/core/services/mpv.test.ts src/core/services/mpv-protocol.test.ts src/main/runtime/mpv-client-event-bindings.test.ts src/main/runtime/mpv-main-event-bindings.test.ts src/main/runtime/mpv-main-event-main-deps.test.ts`
- `bun run typecheck`
- `bun run changelog:lint`
- `bun test src/core/services/subsync.test.ts --test-name-pattern "deterministic _retimed"`
Blocked broader gate:
- `bun run test:fast` failed in `test:core:src` with Bun/node:test nested-test runner errors across unrelated files; the concrete subsync failure from that run passed when isolated.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,35 @@
---
id: TASK-317
title: Add browser open affordance for texthooker
status: Done
assignee: []
created_date: '2026-05-03 02:02'
updated_date: '2026-05-03 02:21'
labels:
- feature
- texthooker
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add a `-o` flag to the texthooker subcommand to open the texthooker page in the user's default browser, and add a tray app option that triggers the same behavior. Implement with tests and existing launcher/tray patterns.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `texthooker -o` starts/targets the texthooker page and opens it in the default browser.
- [x] #2 Tray app exposes a menu option to open the texthooker page in the default browser.
- [x] #3 Existing texthooker behavior without `-o` remains unchanged.
- [x] #4 Relevant CLI/tray behavior covered by tests.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented `subminer texthooker -o` by parsing the launcher subcommand flag, forwarding `--open-browser` to the app texthooker command, and allowing that app arg to force browser opening even when `texthooker.openBrowser` is false. Added an `Open Texthooker` tray menu item wired through the same CLI command path. Updated docs-site usage/launcher/API docs and added a changelog fragment. Verification: targeted CLI/tray tests passed; `bun run typecheck` passed; `bun run docs:test` passed; `bun run changelog:lint` passed; `bun run test:env` passed; `bun run build` passed; `bun run test:smoke:dist` passed; `bun run docs:build` passed after installing docs-site deps. `bun run test:fast` is blocked by an existing broader-suite failure in `runSubsyncManual writes deterministic _retimed filename when replace is false` (`window.electronAPI` undefined), followed by Bun nested-test cascade errors.
Follow-up fix: `subminer texthooker -o` now opens `http://127.0.0.1:5174` from the launcher after a successful texthooker app handoff, so it works even when the installed SubMiner app binary does not yet understand the app-side `--open-browser` flag. Reproduced the reported behavior; confirmed the texthooker server was running at `127.0.0.1:5174`; added a launcher regression asserting the browser URL is opened. Verification: `bun test launcher/mpv.test.ts launcher/config/cli-parser-builder.test.ts launcher/config/args-normalizer.test.ts src/core/services/cli-command.test.ts src/main/runtime/tray-runtime.test.ts src/main/runtime/tray-main-actions.test.ts` passed; `bun run typecheck` passed; `bun run build:launcher` passed.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,53 @@
---
id: TASK-318
title: Keep JLPT underline color fixed after lookup selection
status: Done
assignee:
- '@Codex'
created_date: '2026-05-03 03:17'
updated_date: '2026-05-03 03:19'
labels:
- overlay
- jlpt
- renderer
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Looking up a subtitle token can leave browser/Yomitan selection styling active. If that token has a JLPT class and another annotation class, the underline must remain the JLPT level color because underline color represents static JLPT classification, not the currently active annotation or lookup state.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 JLPT subtitle underlines retain their configured N1-N5 color after lookup/selection styling is applied.
- [x] #2 JLPT tokens that also have known, N+1, name, or frequency annotation classes keep their annotation text color behavior without changing the JLPT underline color.
- [x] #3 Renderer regression coverage verifies the CSS contract for the combined JLPT plus annotation case.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused CSS regression in `src/renderer/subtitle-render.test.ts` for JLPT tokens combined with higher-priority annotation classes and lookup/selection styling.
2. Run the focused renderer test and confirm it fails because selection rules do not lock `text-decoration-color`.
3. Update `src/renderer/style.css` to explicitly preserve JLPT underline decoration color in lookup/selection state selectors without changing text color priority.
4. Re-run the focused renderer test, then run the smallest relevant verification gate.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Verified TDD red/green for renderer CSS contract: `bun test src/renderer/subtitle-render.test.ts` first failed because `word-jlpt-n1::selection` lock was missing, then passed after adding explicit JLPT `text-decoration-color` selection rules. Also ran `bun run changelog:lint` and `bun run typecheck` successfully.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed JLPT subtitle underline color drift after dictionary lookup/selection by adding explicit `::selection` decoration-color locks for N1-N5 token classes in `src/renderer/style.css`. This preserves the JLPT underline as static classification while leaving known/N+1/name/frequency text color priority intact.
Added renderer CSS regression coverage for the JLPT selection lock and a user-visible changelog fragment.
Checks: `bun test src/renderer/subtitle-render.test.ts`; `bun run changelog:lint`; `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-319
title: Suppress annotations for expressive interjection subtitles
status: Done
assignee:
- Codex
created_date: '2026-05-03 03:18'
updated_date: '2026-05-03 03:20'
labels:
- bug
- subtitle-annotations
dependencies: []
references:
- src/core/services/tokenizer/subtitle-annotation-filter.ts
- src/core/services/tokenizer/annotation-stage.test.ts
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Interjection-only subtitle tokens such as ハァ and はっ should remain hoverable as tokens but must not receive known, N+1, frequency, or JLPT annotation styling. Current behavior can still annotate these forms when dictionary/POS metadata does not trip the existing exclusion gate.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Standalone ハァ/はっ-style interjection tokens have annotation metadata cleared even when dictionary metadata exists.
- [x] #2 Filtering remains scoped so content-bearing non-interjection tokens still receive annotations.
- [x] #3 Regression coverage exercises the reported subtitle pattern: ハァ… / (ガーフィール)はっ!
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add failing regression coverage around annotation filtering for the reported interjection forms, including katakana ハァ and small-tsu はっ with surrounding subtitle punctuation/name text.
2. Tighten the shared subtitle annotation exclusion gate so expressive kana interjections clear annotation metadata without relying only on MeCab pos1=感動詞.
3. Run the focused tokenizer/annotation tests, then update acceptance criteria and notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented via shared subtitle annotation exclusion term normalization: added はぁ so katakana ハァ normalizes into the existing term gate. Existing small-tsu kana SFX logic already covers はっ. Regression confirms both reported forms clear known/N+1/frequency/JLPT metadata while a normal noun keeps frequency annotation.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added a regression for the reported subtitle pattern ハァ… / (ガーフィール)はっ!, with annotation metadata present on both interjection tokens.
- Extended the shared subtitle annotation exclusion term set so ハァ normalizes to はぁ and is stripped of annotation styling. Existing はっ handling remains covered by small-tsu kana SFX filtering.
- Added a change fragment for the user-visible bug fix.
Verification:
- bun test src/core/services/tokenizer/annotation-stage.test.ts
- bun test src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer.test.ts src/renderer/subtitle-render.test.ts
- bun run typecheck
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-320
title: Refresh current subtitle known-word highlight after successful mining
status: Done
assignee:
- Codex
created_date: '2026-05-03 03:22'
updated_date: '2026-05-03 03:29'
labels:
- bug
- anki
- subtitle-annotations
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
After a sentence card is mined successfully, the mined word is added to the known-word cache and future subtitle appearances render as known. The currently displayed subtitle must also be refreshed immediately so the mined word turns known-color without waiting for a later cue.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Successful sentence-card mining refreshes the current displayed subtitle so newly mined known words render immediately.
- [x] #2 Unsuccessful/no-op mining does not refresh the current subtitle.
- [x] #3 Regression coverage verifies the successful and unsuccessful mining paths.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a regression test around AnkiIntegration known-word cache appends: when mined note info changes known words, a callback fires.
2. Make KnownWordCacheManager.appendFromNoteInfo report whether it changed the immediate known-word cache.
3. Add an AnkiIntegration known-word-cache-updated callback and invoke it after successful immediate append.
4. Wire main process callback to subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText), forcing active-line retokenization after popup/proxy or local mining updates the known-word cache.
5. Add a changelog fragment and run targeted tests plus typecheck.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented generic known-word-cache update notification instead of shortcut-only refresh. KnownWordCacheManager.appendFromNoteInfo now returns whether in-memory known words changed; AnkiIntegration notifies a callback after successful append. Main process wires that callback to subtitleProcessingController.refreshCurrentSubtitle(appState.currentSubText), forcing retokenization without using stale prefetch/cache data. Added regression coverage in anki-integration.test.ts.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added a known-word-cache update callback on AnkiIntegration and wired it in the main process to refresh the current subtitle after mined note info changes known words.
- Made KnownWordCacheManager.appendFromNoteInfo report whether it changed the known-word cache, so refresh only happens after an actual immediate known-word append.
- Added regression coverage proving mined note info updates known words and emits the update notification.
Verification:
- bun test src/anki-integration.test.ts src/anki-integration/known-word-cache.test.ts src/main/runtime/anki-actions.test.ts src/main/runtime/anki-actions-main-deps.test.ts
- bun run typecheck
- bun run changelog:lint currently blocked by pre-existing invalid metadata in changes/319-interjection-annotation-filter.md.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,63 @@
---
id: TASK-321
title: Preserve word audio during manual clipboard card updates
status: Done
assignee:
- '@Codex'
created_date: '2026-05-03 06:22'
updated_date: '2026-05-03 06:23'
labels:
- anki
- mining
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Manual Ctrl+Shift+C/Ctrl+V card updates on already-mined cards should refresh the sentence content and generated sentence media without removing or replacing the existing word/expression audio. The word is unchanged in this flow, so the configured word audio field must be left untouched while sentence audio remains forced-overwrite behavior from TASK-299.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Manual clipboard subtitle update replaces the resolved sentence audio field with newly generated sentence audio.
- [x] #2 Manual clipboard subtitle update does not include the configured word/expression audio field in Anki field updates.
- [x] #3 Animated image generation still uses the existing word audio duration for lead-in sync when configured.
- [x] #4 A regression test covers preserving word/expression audio during manual clipboard update.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Update the focused manual clipboard card update regression so generated audio is written only to the resolved sentence audio field and the configured word/expression audio field is absent from updateNoteFields payloads.
2. Run the focused test and confirm it fails for the existing TASK-299 behavior.
3. Change CardCreationService.updateLastAddedFromClipboard to stop merging/updating expression audio while preserving forced overwrite for sentence audio.
4. Run the focused test; then run adjacent Anki card-creation tests if the focused gate passes.
5. Update task acceptance criteria/final notes with verification results.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented narrow manual clipboard update change in CardCreationService.updateLastAddedFromClipboard: generated audio now force-overwrites only the resolved sentence audio field and no longer writes the configured word/expression audio field. Animated AVIF lead-in still runs from the original note info before image generation, preserving existing word-audio sync behavior.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Manual Ctrl+Shift+C/Ctrl+V card updates now leave the configured word/expression audio field untouched while force-replacing the resolved sentence audio field.
- Updated the regression test to assert the Anki update payload omits ExpressionAudio and only merges SentenceAudio with forced overwrite.
- Updated docs-site behavior notes and added a changelog fragment for the sentence-only manual audio replacement behavior.
Verification:
- bun test src/anki-integration/card-creation-manual-update.test.ts src/anki-integration/card-creation.test.ts src/anki-integration/animated-image-sync.test.ts
- bun run typecheck
- bun run docs:test
- bun run docs:build
- git diff --check -- src/anki-integration/card-creation.ts src/anki-integration/card-creation-manual-update.test.ts docs-site/mining-workflow.md docs-site/anki-integration.md docs-site/configuration.md changes/322-preserve-word-audio-manual-update.md
Blocked gate:
- bun run changelog:lint is blocked by pre-existing malformed changes/319-interjection-annotation-filter.md, which is outside this task's files.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-322
title: Fix failing CI checks on PR 57
status: Done
assignee:
- codex
created_date: '2026-05-03 06:27'
updated_date: '2026-05-03 06:31'
labels:
- ci
- bug
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/57'
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Investigate and fix failing GitHub Actions checks on PR #57 (`tokenizer-updates`). Scope: use CI logs to identify root cause, apply focused local fix, and verify with relevant local checks.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Failing GitHub Actions check root cause is identified from logs.
- [x] #2 A focused code/test/docs fix is applied locally.
- [x] #3 Relevant local verification passes or blocked reason is documented.
- [x] #4 PR checks are rechecked or next CI action is documented.
- [x] #5 Actionable CodeRabbit PR comments are inspected and addressed or documented as non-actionable.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Fix CI changelog lint by adding a valid `type` frontmatter value to `changes/319-interjection-annotation-filter.md`.
2. Address unresolved CodeRabbit threads:
- `scripts/test-plugin-session-bindings.lua`: make `.tmp` creation portable across Unix/Windows shells.
- `src/core/services/tokenizer.ts`: pass `TokenizerAnnotationOptions` through `stripSubtitleAnnotationMetadata` paths so `sourceText` is honored.
- `src/main/runtime/mpv-main-event-main-deps.ts`: align overlay-runtime quit-on-disconnect predicate with `hasInitialPlaybackQuitOnDisconnectArg`.
- `src/renderer/handlers/mouse.test.ts`: make `elementFromPoint` stubs coordinate-sensitive.
3. Run focused checks: `bun run changelog:lint`, relevant tokenizer/main/mouse tests, and plugin Lua test path if available.
4. Recheck PR checks/comments after local verification.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
CI root cause: GitHub Actions `build-test-audit` failed during `bun run changelog:lint`; `changes/319-interjection-annotation-filter.md` must declare `type` as one of `added`, `changed`, `fixed`, `docs`, `internal`. Scope expanded by user to also address CodeRabbit comments on PR #57.
Implemented CI changelog metadata fix and unresolved CodeRabbit feedback locally. Full verification run: `bun run changelog:lint`, focused tests, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`, `bun run format:check:src`. Rechecked PR checks: remote `build-test-audit` still shows the old failing run until this branch is pushed; CodeRabbit remains pending remotely until review reruns.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed PR #57 CI failure by converting `changes/319-interjection-annotation-filter.md` to valid changelog fragment metadata. Addressed unresolved CodeRabbit feedback by making plugin test `.tmp` creation portable, threading tokenizer annotation options through metadata stripping, aligning quit-on-disconnect predicates for Jellyfin playback, and strengthening mouse hit-test assertions. Also formatted two existing PR files required by the source format gate. Verification passed locally: changelog lint, focused tests, typecheck, test:fast, test:env, build, smoke dist, and format check. Remote PR checks still show the previous failed `build-test-audit` run until these local changes are pushed.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,60 @@
---
id: TASK-323
title: Fix macOS overlay hiding while mpv remains active
status: Done
assignee:
- '@codex'
created_date: '2026-05-03 07:41'
updated_date: '2026-05-03 07:48'
labels:
- bug
- macos
- overlay
dependencies: []
references:
- src/core/services/overlay-visibility.ts
- src/window-trackers/macos-tracker.ts
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
macOS visible overlay can hide/reload during normal playback even while mpv, or the overlay over mpv, remains the active viewing surface. The fix should preserve overlay visibility and subtitle continuity during transient macOS focus/tracker flaps, while still hiding the overlay when the tracked mpv window is genuinely unavailable or another app is brought forward.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 When the macOS tracker has recent valid mpv geometry, transient focus/helper misses do not hide the visible overlay or force a reload.
- [x] #2 The overlay still hides when the tracked mpv window is genuinely lost beyond the existing tracking grace behavior.
- [x] #3 A regression test covers the macOS active-playback case where mpv/overlay focus is preserved despite a transient non-tracking state.
- [x] #4 Relevant docs or task notes are updated if behavior or verification guidance changes.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a failing regression in `src/core/services/overlay-visibility.test.ts`: on macOS, after the overlay is visible/tracked, a transient tracker state with `isTracking() === false` but non-null `getGeometry()` keeps the overlay visible, updates bounds, and does not call `hide()` or loading OSD.
2. Implement the minimal macOS preserve path in `src/core/services/overlay-visibility.ts`, mirroring the existing Windows transient non-minimized branch but without Windows z-order binding.
3. Preserve existing startup/lost-window behavior: `windowTracker: null` and `isTracking() === false` with `getGeometry() === null` still hide and show the first loading OSD.
4. Run focused tests for `src/core/services/overlay-visibility.test.ts`; then typecheck or the repo runtime verification lane if the focused patch passes.
5. Update TASK-323 notes/acceptance criteria with verification results.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Added a macOS overlay visibility regression for transient tracker loss with retained geometry. The test failed first because the old path marked tracker-not-ready and hid the overlay. Implemented a scoped preserve path in `src/core/services/overlay-visibility.ts`: macOS now keeps the visible overlay alive only when the tracker still has retained geometry; true loss with null geometry still hides and emits the existing loading OSD behavior. Added changelog fragment `changes/323-macos-overlay-tracker-flaps.md`.
Verification: `bun test src/core/services/overlay-visibility.test.ts` passed after the fix; `bun test src/window-trackers/macos-tracker.test.ts src/core/services/overlay-visibility.test.ts` passed; `bun run typecheck` passed; `bun run test:env` passed; isolated `bun test src/core/services/subsync.test.ts` passed; `bun run build` passed; `bun run test:smoke:dist` passed; `bun run changelog:lint` passed. `bun run test:fast` failed twice in an unrelated broad-suite interaction where `src/renderer/handlers/keyboard.ts` tried to use missing `window.electronAPI` while `src/core/services/subsync.test.ts` was running, followed by Bun node:test nested-test cascade errors.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed the macOS visible-overlay hide/reload path during normal playback by preserving the overlay when the tracker briefly reports non-tracking but still has retained mpv geometry. The overlay visibility service now treats that macOS state like a transient tracker flap: it keeps bounds/layer/order refreshed and leaves the overlay click-through instead of hiding or showing the loading OSD. True macOS loss remains unchanged: no tracker or null geometry still hides the overlay and uses the existing loading behavior.
Added regression coverage in `src/core/services/overlay-visibility.test.ts` for the active-playback case and added changelog fragment `changes/323-macos-overlay-tracker-flaps.md`.
Verification passed: focused overlay tests, macOS tracker + overlay tests, typecheck, `test:env`, isolated `subsync.test.ts`, build, dist smoke, and changelog lint. Full `test:fast` remains blocked by an unrelated broad-suite interaction where renderer keyboard state fires without `window.electronAPI` during `subsync.test.ts`, then Bun reports node:test cascade errors.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,63 @@
---
id: TASK-324
title: Fix mpv playlist changes re-running app warmups
status: Done
assignee: []
created_date: '2026-05-03 07:48'
updated_date: '2026-05-03 07:52'
labels:
- bug
- mpv
- overlay
dependencies: []
references:
- launcher/
- src/core/services/mpv.ts
- src/main/runtime/
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When moving to the next or previous mpv playlist entry, SubMiner should reconnect the existing app/runtime to mpv instead of treating the new video like a fresh app startup. Re-running startup warmups or creating another app session after the first video can interfere with overlay behavior.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Changing to next or previous mpv playlist item reuses the existing app/runtime instead of launching a new app session.
- [x] #2 Startup warmups are not repeated for playlist item changes after the first app startup.
- [x] #3 Overlay behavior remains available after playlist navigation.
- [x] #4 Regression test covers the playlist-change/reconnect path.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Reproduce the plugin auto-start regression with a failing Lua start-gate test.
2. Update mpv plugin auto-start handling so playlist/file changes with an already-running overlay reuse the existing app path and do not re-arm pause-until-ready warmup.
3. Add changelog fragment and run plugin/launcher verification.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
RED: `lua scripts/test-plugin-start-gate.lua` failed after changing the duplicate pause-until-ready auto-start expectations; it showed the loading gate was armed twice while overlay was already running.
GREEN: `plugin/subminer/process.lua` now disarms any old ready gate and only reasserts visible overlay state when auto-start fires while `state.overlay_running` is already true.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Updated the mpv Lua plugin auto-start reuse path so a file/playlist load with an already-running overlay no longer re-arms the pause-until-ready tokenization gate.
- Kept the existing app/control command reuse behavior: subsequent auto-starts reassert visible/hidden overlay state without issuing another `--start` subprocess.
- Added a changelog fragment for the mpv playlist overlay reuse fix.
Tests:
- `lua scripts/test-plugin-start-gate.lua` (red before fix, green after)
- `bun run test:plugin:src`
- `bun run changelog:lint`
- `bun run test:launcher:env:src`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,55 @@
---
id: TASK-325
title: Fix keyboard-only Yomitan popup shortcut precedence
status: Done
assignee:
- codex
created_date: '2026-05-04 01:19'
updated_date: '2026-05-04 01:22'
labels:
- bug
- keyboard
- yomitan
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When keyboard-only mode is active and a Yomitan popup is visible, popup keyboard controls must win over overlay/mpv/session keybindings. Currently default overlay bindings such as bare `j` can fire instead of scrolling/navigating the Yomitan popup.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 With keyboard-only mode active and a Yomitan popup visible, pressing `j`/`k` forwards to the Yomitan popup instead of dispatching default session bindings such as primary subtitle track cycling.
- [x] #2 With keyboard-only mode inactive, existing popup-visible session binding behavior remains unchanged for bound keys.
- [x] #3 Regression coverage captures the keyboard-only popup precedence behavior.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression in `src/renderer/handlers/keyboard.test.ts`: keyboard-only mode + visible Yomitan popup + bare `KeyJ` session binding should forward `KeyJ` to the popup and not dispatch the mpv/session binding.
2. Verify the new test fails before production changes.
3. Patch `src/renderer/handlers/keyboard.ts` so popup key handling ignores session-binding precedence only while keyboard-driven mode is enabled.
4. Run targeted renderer keyboard tests, then update acceptance criteria and final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implementation: `handleYomitanPopupKeybind` now only lets configured session bindings take precedence when keyboard-driven mode is not enabled. In keyboard-only mode with a visible Yomitan popup, bare popup keys such as `KeyJ` forward to the popup instead of dispatching overlay/mpv keybindings. Added a regression covering `KeyJ` bound to `cycle sid`.
Verification: targeted test failed before the production change, then passed after the fix. Full local gates run: `bun test src/renderer/handlers/keyboard.test.ts --test-name-pattern "keyboard mode: popup keybinds take precedence"`, `bun test src/renderer/handlers/keyboard.test.ts`, `bun run changelog:lint`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`. Build initially required `bun install --frozen-lockfile`, submodule init, and `stats/` locked deps install because this worktree had no dependencies/submodules checked out.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed keyboard-only Yomitan popup shortcut precedence by allowing popup key forwarding to bypass configured session bindings only while keyboard-driven mode is active. This makes popup controls such as `j`/`k` win over default overlay/mpv bindings like primary subtitle track cycling, while preserving existing non-keyboard-only popup behavior where configured bindings still fire.
Added renderer keyboard regression coverage for the reported `KeyJ`/`cycle sid` conflict and added a changelog fragment for the user-visible overlay fix.
Verification passed: targeted red/green regression, full renderer keyboard test file, changelog lint, typecheck, `test:fast`, `test:env`, build, and dist smoke tests.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,38 @@
---
id: TASK-325
title: Fix session chart known-word percentage denominator
status: Done
assignee: []
created_date: '2026-05-04 01:19'
updated_date: '2026-05-04 01:23'
labels:
- stats
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Session detail known-word percentages should use the same filtered vocabulary occurrence rows for both known and total word counts. Current chart can divide known persisted word occurrences by raw token totals, causing excluded tokens to depress the known percentage.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Session known-word timeline API exposes cumulative filtered total word counts alongside known counts.
- [x] #2 Session detail chart computes known/unknown areas from filtered totals, not raw timeline token counts, when known-word data is available.
- [x] #3 Session summary known-word rate uses filtered persisted word totals where available and preserves safe fallback behavior when known-word data is unavailable.
- [x] #4 Regression tests cover filtered denominator behavior for the API and chart data path.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented in-place fix using existing persisted word occurrence rows. `/api/stats/sessions/:id/known-words-timeline` now returns cumulative `totalWordsSeen` from filtered persisted occurrences, and session known-word rates divide by the same filtered total. Session detail chart builds known/unknown areas from `totalWordsSeen` instead of raw timeline `tokensSeen`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Known-word percentages on session charts now use filtered persisted word totals for both numerator and denominator. No migration/backfill required; data comes from existing `imm_word_line_occurrences`. Added regression coverage for the API response/rate and chart data builder.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,56 @@
---
id: TASK-325
title: Keep JLPT underline color fixed with combined lookup annotations
status: Done
assignee:
- '@Codex'
created_date: '2026-05-04 00:25'
updated_date: '2026-05-04 00:28'
labels:
- overlay
- jlpt
- renderer
dependencies: []
references:
- TASK-318
- TASK-308
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Yomitan lookup on a subtitle token that has a JLPT level plus another annotation such as frequency or known-word highlighting can make the JLPT underline take the other annotation color. The underline must always remain the token's JLPT level color; other annotation classes may still control text color.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A JLPT token combined with frequency styling keeps its underline set to the configured JLPT level color during lookup/selection styling.
- [x] #2 A JLPT token combined with known-word styling keeps its underline set to the configured JLPT level color during lookup/selection styling.
- [x] #3 Regression coverage exercises combined JLPT plus non-JLPT annotation selectors, including character span selection/hover styling used by lookup.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused renderer CSS regression coverage for combined `word-jlpt-n*` plus known/frequency classes, including `.c::selection`/`.c:hover` lookup paths.
2. Run `bun test src/renderer/subtitle-render.test.ts` and confirm the new assertion fails on the current CSS.
3. Update `src/renderer/style.css` so JLPT decoration color is locked on the token and child character spans without changing text color priority for known/frequency/name/N+1 annotations.
4. Re-run the focused renderer test, then run typecheck/changelog checks as scope requires.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Added red/green renderer CSS regression for combined JLPT plus known/N+1/frequency annotation classes and character hover lookup paths. Current CSS failed before the lock selectors were added; focused test passes after the CSS change.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed JLPT underline color drift for tokens that also carry known-word, N+1, or frequency annotation classes. The renderer CSS now explicitly locks the underline decoration color for combined JLPT annotation selectors, hover, character hover, and selection states while preserving the existing text color priority for other annotations.
Added renderer regression coverage for combined JLPT plus non-JLPT annotation selectors and lookup character hover paths. Added a user-visible changelog fragment.
Checks: `bun test src/renderer/subtitle-render.test.ts`; `bun run changelog:lint`; `bun run typecheck`; `bun run format:check:src`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,32 @@
---
id: TASK-326
title: Fix AniList post-watch update after skipped completion threshold
status: In Progress
assignee: []
created_date: '2026-05-04 00:33'
labels:
- anilist
- bug
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
AniList episode progress should sync reliably when playback reaches or passes the watched trigger point, even if mpv progress events jump over the exact threshold. Investigate why a completed watched episode did not update AniList and fix the root cause for post-watch tracking.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 When playback moves from before the completion threshold to any later position at or beyond the threshold, AniList queues or sends the episode progress update once.
- [x] #2 If playback is already past the completion threshold and the update has not yet been recorded for the current media/episode, AniList still queues or sends the update.
- [x] #3 AniList progress updates remain deduplicated for the same media/episode watch completion.
- [x] #4 A regression test covers the skipped-threshold or already-past-threshold case.
<!-- AC:END -->
## Notes
- Fixed mpv `time-pos` ordering so post-watch checks read the fresh playback position after seeks.
- Wired manual mark-watched to run a forced AniList post-watch sync after the local watched mark succeeds.
- Added regressions for time-position ordering, manual watched sync, forced post-watch updates, and the Little Witch Academia filename parse.
@@ -0,0 +1,42 @@
---
id: TASK-326
title: Make stats word metrics honor filtering rules
status: Done
assignee: []
created_date: '2026-05-04 01:35'
updated_date: '2026-05-04 02:08'
labels:
- stats
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Audit stats app metrics that show or derive from word totals and make them use filtered persisted vocabulary occurrences where the UI concept is learned/seen words. Preserve raw telemetry only where it is intentionally playback/token telemetry.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Stats UI word totals, word rates, lookup-per-word rates, and chart word series use filtered persisted word occurrences where available.
- [x] #2 Known-word metrics continue to use the same filtered denominator as known counts.
- [x] #3 Trend, overview, library, session, and episode surfaces are audited with regression coverage for changed data paths.
- [x] #4 Fallback behavior remains safe for sessions without persisted vocabulary occurrences.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Audit finding: raw `tokensSeen` / `totalTokensSeen` still feeds overview hints, dashboard aggregation, trends activity/progress/anime cumulative/library summary, lookup-per-100-word rates, session rows/recent sessions/episode sessions, and library/anime/media headers. Vocabulary and known unique word summaries already use persisted filtered vocabulary rows. Recommended design: query-time filtered word totals from existing `imm_word_line_occurrences`, with raw-token fallback only when a session has no persisted occurrence rows.
Implemented shared query-time filtered word counts. Session summaries, overview hints, daily/monthly rollups, anime/media library/detail rows, anime episode rows, episode/media sessions, trends activity/progress/anime cumulative, library summary, and lookup-per-100-word ratios now use filtered persisted word occurrences. Fallback remains raw token totals only for sessions with no persisted subtitle-line rows.
Follow-up implemented: Vocab frequency tables now apply the same tokenizer vocabulary predicate at read time, because old `imm_words` rows can predate current tokenizer exclusion rules. Vocabulary persistence and cleanup also mirror the broader subtitle-annotation grammar filters. Added common frequency stop terms observed in the stats vocabulary list to the shared tokenizer exclusion set so those rows are filtered consistently across subtitle annotations, persistence, cleanup, stats reads, and SQL word-count aggregates.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Stats word metrics now honor filtering rules through the read-model query layer. Existing persisted `imm_word_line_occurrences` provide the filtered denominator; no migration/backfill needed. Vocab tables filter stored rows on read using tokenizer vocabulary rules, so legacy noisy rows stop appearing without a migration. Added regressions for session/overview/rollup fallback behavior, trends/library lookup-rate behavior, vocabulary read filtering, cleanup filtering, and shared stop-term filtering.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,42 @@
---
id: TASK-327
title: Persist stats page exclusion list in database
status: Done
assignee: []
created_date: '2026-05-04 01:39'
updated_date: '2026-05-04 01:49'
labels:
- feature
- stats
- database
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add database-backed persistence for the stats page exclusion list. On first load with the new schema, seed the new table from the existing exclusion list source so existing user choices are preserved. After migration, update database rows whenever the exclusion list is changed or saved so it persists across browser sessions indefinitely.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A new small database table stores stats page exclusion entries.
- [x] #2 First load with the new schema seeds the table from the existing exclusion list source.
- [x] #3 Subsequent exclusion list save/change operations update the database-backed list.
- [x] #4 Regression coverage verifies migration/seed behavior and persistence updates.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented DB-backed stats exclusion list using schema version 18 and new `imm_stats_excluded_words` table. Added read/replace query helpers, service methods, and `/api/stats/excluded-words` GET/PUT routes. Stats frontend now loads exclusions from DB, seeds the empty DB table from legacy `localStorage` on first load, and writes each toggle/restore/clear through the API while keeping localStorage in sync for compatibility. Added focused regression coverage for schema/read-replace, API routes, API client, and frontend bootstrap/update behavior. Verification: `bun run typecheck` passed; `bun test src/core/services/__tests__/stats-server.test.ts stats/src/lib/api-client.test.ts stats/src/hooks/useExcludedWords.test.ts` passed; `bun test src/core/services/immersion-tracker/storage-session.test.ts` passed; `bun run docs:test` passed; `bun run format:check:stats` passed; `bun run changelog:lint` passed. Blocked/unrelated: `bun run typecheck:stats` fails in existing stats files (`AnilistSelector.tsx`, `reading-utils*`, `session-grouping.test.ts`, `yomitan-lookup.test.tsx`); `bun run test:immersion:sqlite:src` fails existing `recordSubtitleLine counts exact Yomitan tokens for session metrics` expected 4 got 3; `bun run docs:build` fails missing `@catppuccin/vitepress/theme/macchiato/mauve.css` import.
Added `src/core/services/__tests__/stats-server.test.ts` and `stats/src/hooks/useExcludedWords.test.ts` to the `test:core:src` allowlist so the new DB exclusion route/client/store regressions run in the maintained fast source lane.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Persisted the stats vocabulary exclusion list in SQLite with new schema version 18 table `imm_stats_excluded_words`. Added backend read/replace helpers and `/api/stats/excluded-words` GET/PUT routes, then wired the stats frontend exclusion store to load DB rows, seed an empty DB from legacy browser localStorage on first load, and update the DB on toggle/restore/clear. Updated docs and added changelog fragment. Focused tests and root typecheck pass; broader stats/docs/sqlite gates are blocked by unrelated existing failures recorded in notes.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,67 @@
---
id: TASK-327
title: Restore stats daemon deferral when launching playback
status: Done
assignee:
- '@Codex'
created_date: '2026-05-04 01:15'
updated_date: '2026-05-04 01:17'
labels:
- bug
- stats
- runtime
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Launching a video while a background stats daemon is already running must not fail with stats.serverPort already in use. Normal in-app stats startup should reuse the live daemon URL instead of binding a second stats server, while preserving managed playback shutdown behavior from TASK-316.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A live background stats daemon from another process causes in-app stats URL resolution to return the daemon URL without starting a local stats server.
- [x] #2 Dead or stale daemon state is removed and local stats startup still works.
- [x] #3 Managed playback shutdown behavior remains covered by existing tests.
- [x] #4 Focused regression tests pass.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Update `src/main/runtime/stats-server-routing.test.ts` first so a live foreign daemon must return its daemon URL and skip local server startup.
2. Run the focused routing test to confirm the regression fails red.
3. Update `src/main/runtime/stats-server-routing.ts` to return `{ source: 'background' }` for live foreign daemon state, clear stale/self-owned state, and keep local startup fallback unchanged.
4. Run focused stats routing tests plus managed playback tests touched by TASK-316.
5. Update changelog and task acceptance/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented via TDD: first changed `stats-server-routing.test.ts` to require live foreign daemon deferral and observed the expected red failure. Then restored `stats-server-routing.ts` to return the daemon URL with `source: 'background'` when daemon state belongs to a live other process. Stale/dead and self-owned stale cleanup paths remain local fallback.
Verification passed: `bun test src/main/runtime/stats-server-routing.test.ts`; focused runtime suite for stats daemon + TASK-316 managed playback files; `bun run typecheck`; `bun run test:fast`.
`bun run changelog:lint` is blocked by pre-existing unrelated `changes/326-anilist-time-position-post-watch.md` missing valid `type` metadata; `changes/327-stats-daemon-deferral.md` follows the expected fragment format.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Restored in-app stats startup deferral to a live background stats daemon from another process, returning the daemon URL and skipping local stats server binding.
- Kept stale/dead daemon cleanup and local stats startup fallback behavior intact.
- Added a changelog fragment for the restored port-conflict fix.
Verification:
- `bun test src/main/runtime/stats-server-routing.test.ts`
- `bun test src/main/runtime/stats-server-routing.test.ts src/core/services/mpv.test.ts src/core/services/mpv-protocol.test.ts src/main/runtime/mpv-client-event-bindings.test.ts src/main/runtime/mpv-main-event-bindings.test.ts src/main/runtime/mpv-main-event-main-deps.test.ts src/main/runtime/stats-cli-command.test.ts src/stats-daemon-control.test.ts`
- `bun run typecheck`
- `bun run test:fast`
Blocked check:
- `bun run changelog:lint` fails on unrelated pre-existing `changes/326-anilist-time-position-post-watch.md` metadata, not this change.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,64 @@
---
id: TASK-328
title: Keep subtitle prefetch running after immediate cached annotation render
status: Done
assignee:
- codex
created_date: '2026-05-04 01:26'
updated_date: '2026-05-04 01:30'
labels: []
dependencies: []
references:
- >-
/home/sudacode/projects/japanese/SubMiner/src/main/runtime/mpv-main-event-actions.ts
- /home/sudacode/projects/japanese/SubMiner/src/main.ts
- >-
/home/sudacode/projects/japanese/SubMiner/src/core/services/subtitle-processing-controller.ts
- >-
/home/sudacode/projects/japanese/SubMiner/backlog/completed/task-197 -
Eliminate-per-line-plain-subtitle-flash-on-prefetch-cache-hit.md
- >-
/home/sudacode/projects/japanese/SubMiner/backlog/completed/task-196 -
Fix-subtitle-prefetch-cache-key-mismatch-and-active-cue-window.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Cached subtitle annotation hits should render annotated subtitles immediately without starving the subtitle prefetcher. Current evidence: the mpv subtitle-change path emits the cached payload before forwarding the subtitle change; in the runtime, the cached emit resumes prefetch, then the forwarded change pauses it, and no async controller emit follows on a cache hit to resume it again.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Cached subtitle annotation payloads still render immediately without a plain subtitle flash.
- [x] #2 A cache-hit subtitle-change event leaves subtitle prefetch eligible to continue after the immediate annotated emit.
- [x] #3 Cache-miss subtitle-change behavior still shows plain text immediately while async annotation processing runs.
- [x] #4 Regression coverage proves the cache-hit ordering that prevents prefetch from staying paused.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression test in `src/main/runtime/mpv-main-event-actions.test.ts` proving cache-hit subtitle changes pause live prefetch work before emitting the immediate annotated payload, so the emit resumes prefetch last.
2. Change `createHandleMpvSubtitleChangeHandler` ordering in `src/main/runtime/mpv-main-event-actions.ts`: set current text, consume cache, forward `onSubtitleChange(text)`, then emit cached payload or plain fallback, then refresh Discord presence.
3. Preserve existing behavior: cache hits emit annotated payload synchronously; cache misses emit `{ text, tokens: null }` synchronously.
4. Run focused tests for `mpv-main-event-actions`; run adjacent controller/prefetch tests if ordering touches cache assumptions.
5. Update TASK-328 acceptance criteria and add a changelog fragment if the repo requires one for this user-visible fix.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Red/green: added cache-hit ordering regression in `src/main/runtime/mpv-main-event-actions.test.ts`; first run failed with actual order `emit:annotated` before `process:line`. Fix narrows ordering change to cache hits only: cache hit calls `onSubtitleChange` before immediate annotated emit; cache miss keeps plain broadcast before processing.
Verification: `bun test src/main/runtime/mpv-main-event-actions.test.ts` passed; `bun test src/core/services/subtitle-processing-controller.test.ts` passed; `bun test src/core/services/subtitle-prefetch.test.ts` passed; combined targeted test command passed 35 tests; `bun run typecheck` passed. `bun run changelog:lint` blocked by unrelated pre-existing `changes/326-anilist-time-position-post-watch.md` missing a valid `type` metadata line.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed the subtitle cache-hit ordering that could leave subtitle prefetch paused after an immediate annotated render. Cache hits now forward the subtitle change first, then emit the cached annotated payload, so the runtime pause happens before the emit path resumes prefetch. Cache misses keep the previous plain-subtitle-first path so fallback text still appears immediately while tokenization runs.
Added a regression test for the cache-hit ordering and a changelog fragment for the overlay fix. Verified with targeted subtitle runtime/controller/prefetch tests and `bun run typecheck`; changelog lint is blocked by an unrelated existing malformed fragment for TASK-326.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,43 @@
---
id: TASK-329
title: Keep JLPT subtitle styling underline-only
status: Done
assignee: []
created_date: '2026-05-04 02:13'
labels:
- bug
- renderer
- jlpt
dependencies: []
references:
- src/renderer/style.css
- src/renderer/subtitle-render.test.ts
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Fix subtitle token styling so JLPT metadata never changes token text color. JLPT should only render the level marker/underline affordance while known, n+1, name-match, and frequency colors retain priority.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 JLPT-only subtitle tokens do not set token text color.
- [x] #2 JLPT level marker/underline still uses configured JLPT color.
- [x] #3 Existing known, n+1, name-match, and frequency text colors remain unchanged.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Changed subtitle JLPT styling from text color to underline decoration and updated renderer CSS regression coverage.
Verification:
- `bun test src/renderer/subtitle-render.test.ts`
- `bunx prettier --check src/renderer/subtitle-render.test.ts src/renderer/style.css`
- `bun run typecheck`
Blocked:
- `bun run test:fast` fails in existing dirty stats/session work: `recordSubtitleLine counts exact Yomitan tokens for session metrics` expects `tokensSeen` 4 but gets 3.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,70 @@
---
id: TASK-330
title: Fix PR 60 CI failures and CodeRabbit feedback
status: Done
assignee:
- codex
created_date: '2026-05-04 02:50'
updated_date: '2026-05-04 02:59'
labels:
- ci
- pr-review
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/60'
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Resolve failing GitHub Actions checks and actionable unresolved CodeRabbit review feedback on PR #60 (Persist stats exclusions in DB and fix word metrics filtering). Keep fixes scoped to the PR behavior and preserve existing project patterns.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Failing GitHub Actions checks for PR #60 have an identified root cause and local fix.
- [x] #2 All actionable unresolved CodeRabbit review comments on PR #60 are addressed locally or explicitly documented as non-actionable.
- [x] #3 Relevant local verification passes for the changed code paths.
- [x] #4 Task notes summarize CI failure context, review-comment handling, and any residual verification gaps.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Resolve PR #60 context and inspect GitHub Actions failures with the gh-fix-ci workflow.
2. Fetch unresolved review threads with the gh-address-comments workflow, focusing on CodeRabbit actionable comments.
3. Read the touched files/tests around the failing paths and comments; identify root cause before edits.
4. Apply minimal fixes with regression coverage where appropriate.
5. Run targeted verification first, then broader repo gates as time permits.
6. Update Backlog notes/acceptance criteria with CI/comment outcomes and residual risks.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Resolved PR #60 CI failure by restoring raw `tokensSeen` for session summaries while keeping filtered persisted word counts in aggregate/known-word paths. Addressed CodeRabbit feedback: fixed missing `headword` test fixture binding; paged vocabulary stats past filtered rows; preserved lifetime/rollup totals when retained-session recomputation is partial; emitted flat known-word timeline points for zero-visible-word line gaps; restored localStorage mocks; added rollback/retry behavior for excluded-word store persistence/initialization.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed the PR #60 CI failure and addressed actionable CodeRabbit feedback.
Key changes:
- Restored exact Yomitan token counts for session summary metrics while leaving filtered word counts for aggregate and known-word calculations.
- Fixed malformed query test fixtures by binding `headword` into `imm_words` inserts.
- Updated vocabulary stats to page until enough visible rows are collected after post-query filtering.
- Made library/detail/rollup read models preserve lifetime or stored rollup totals when retained-session recomputation is partial, including dashboard rollup-derived word metrics.
- Kept known-word timeline line positions stable by emitting flat points for missing line indexes.
- Made excluded-word persistence rollback on failed writes, allow initialization retries after transient load failures, and restored mocked `localStorage` in tests.
Verification passed:
- `bun run typecheck`
- `bun run test:fast`
- `bun run test:env`
- `bun run build`
- `bun run test:smoke:dist`
- `bun run format:check:src`
- `git diff --check`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,37 @@
---
id: TASK-331
title: Address unresolved CodeRabbit comments on PR 57
status: Done
assignee:
- codex
created_date: '2026-05-04 03:21'
updated_date: '2026-05-04 03:27'
labels:
- pr-feedback
- coderabbit
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/57'
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Assess and fix unresolved CodeRabbit review comments on PR #57 after rebasing tokenizer-updates. Scope includes manual clipboard SentenceAudio guard, tokenizer standalone particle blacklist, AniList guessit fallback confidence, startup gate duplicate auto-start, and small regression-test hardening where applicable.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Each unresolved CodeRabbit comment is either fixed or explicitly assessed as not applicable against current code.
- [x] #2 Regression tests cover behavior changes where practical.
- [x] #3 Relevant focused tests and typecheck pass.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Fixed all verified actionable CodeRabbit comments from PR #57: manual clipboard updates no longer fall back to ExpressionAudio when SentenceAudio is absent, connective particle phrases no longer suppress lexical verb readings like 立って, guessit output only borrows parser season/episode from non-low-confidence parses, duplicate auto-start no longer releases an active pause-until-ready gate, JLPT CSS tests block text-decoration shorthand underlines, post-watch update rejection logging is covered, and duplicate quit-on-disconnect predicate code is shared.
Verification: bun test src/anki-integration/card-creation-manual-update.test.ts src/core/services/tokenizer/annotation-stage.test.ts src/core/services/anilist/anilist-updater.test.ts src/main/runtime/mpv-main-event-actions.test.ts src/renderer/subtitle-render.test.ts; lua scripts/test-plugin-start-gate.lua; bun run typecheck; bun run test:fast.
<!-- SECTION:NOTES:END -->
@@ -0,0 +1,60 @@
---
id: TASK-332
title: Fix subtitle frequency annotation missing ranks shown in Yomitan popup
status: Done
assignee:
- Codex
created_date: '2026-05-04 03:29'
updated_date: '2026-05-04 03:41'
labels:
- bug
- tokenizer
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Subtitle frequency highlighting can miss a token even when the Yomitan popup shows a rank within the configured threshold. Reproduced with `第二走者とアンカーは\n中継地点に速やかに移動!`: Yomitan popup shows `第二` JPDB rank 1820, but SubMiner tokenizer output has no `frequencyRank` for `第二`, so renderer cannot annotate it.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `第二` in `第二走者とアンカーは\n中継地点に速やかに移動!` receives the Yomitan rank shown by the popup when frequency highlighting is enabled.
- [x] #2 Regression test covers the Yomitan scan/frequency ingestion path for exact popup-derived ranks.
- [x] #3 Existing tokenizer frequency tests continue to pass.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Reproduce and inspect the missing `第二` rank path with tokenizer probes and focused tests.
2. Preserve exact Yomitan scan frequency ranks when the matching frequency entry omits reading metadata but has the same exact term.
3. Allow ranked ordinal prefix-noun tokens (`第` + numeric noun, e.g. `第二`) through annotation POS filtering while keeping standalone prefixes excluded.
4. Verify with focused tokenizer/runtime/annotation tests, typecheck, changelog lint, and a live-style Yomitan profile probe.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Root-cause probe against temp copy of Yomitan profile: tokenizer returns no frequencyRank for `第二`; renderer config `topX` is 10000, so render threshold is not the blocker.
User approved implementation plan on 2026-05-04.
Verification: `bun test src/core/services/tokenizer.test.ts src/core/services/tokenizer/yomitan-parser-runtime.test.ts src/core/services/tokenizer/annotation-stage.test.ts` passed (192 tests).
Verification: `bun run typecheck` passed.
Verification: `bun run changelog:lint` passed.
Verification: `bun run get-frequency:electron -- --yomitan-user-data /tmp/subminer-yomitan-probe-909423 "第二走者とアンカーは\\n中継地点に速やかに移動!"` produced `第二` with `frequencyRank: 1820`.
Finalization check: implementation plan updated to reflect the discovered POS-filter root cause and completed solution.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed subtitle frequency annotation for `第二` by allowing ranked ordinal prefix-noun compounds through annotation POS filtering. Also made scan rank matching tolerate exact frequency entries where one side omits reading metadata. Verified with tokenizer/runtime/annotation tests, typecheck, changelog lint, and a live-style Yomitan profile probe showing `第二` now receives frequencyRank 1820.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,53 @@
---
id: TASK-333
title: Suppress aru subtitle annotations
status: Done
assignee: []
created_date: '2026-05-04 04:39'
updated_date: '2026-05-04 05:02'
labels:
- tokenizer
- annotations
- bug
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add `ある` / `有る` to the subtitle annotation suppression path so `aru` tokens remain hoverable and never receive N+1, JLPT, frequency, or name-match annotation metadata. Known-word highlighting is special: if a filtered `aru` token is known and known highlighting is enabled, it should still render as known.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `ある` and kanji headword/surface variants such as `有る` are excluded by the subtitle annotation filter.
- [x] #2 Annotation stripping clears N+1, JLPT, frequency, and name metadata for `aru` tokens while preserving token hover data.
- [x] #3 Known-word highlighting still applies to filtered tokens, including `aru`, when known-word lookup marks them known.
- [x] #4 Regression coverage fails before the fix and passes after.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add `ある`/`有る`/`在る` to the shared subtitle annotation hard-exclusion terms.
2. Preserve/recompute known-word status for filtered tokens while stripping N+1, JLPT, frequency, and name metadata.
3. Add RED/GREEN unit and tokenizer regression coverage, plus a changelog fragment.
4. Run targeted tests and full handoff gate.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
TDD path: added failing annotation-stage coverage first. Initial implementation made targeted tests pass, then broader tokenizer coverage revealed an older fixture expecting `ある` to remain lexical; updated that integration expectation to the new requested behavior. Follow-up correction: known-word highlighting is the lone annotation exception for filtered tokens, so the strip path now preserves known state and `annotateTokens` recomputes known status for filtered tokens while still clearing N+1/JLPT/frequency/name metadata.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Suppressed non-known subtitle annotations for `aru` existence verbs by adding `ある`, `有る`, and `在る` to the shared hard-exclusion list. Corrected the filtered-token path so known-word highlighting still applies whenever known highlighting is enabled; filtered tokens now keep/gain `isKnown` but still lose N+1, JLPT, frequency, and name metadata.
Added and updated annotation-stage and tokenizer regression coverage for `aru`, particles, helper fragments, interjections, and other filtered known tokens. Added `changes/333-aru-annotation-filter.md`.
Validation passed: RED failures observed before implementation/correction; `bun test src/core/services/tokenizer/annotation-stage.test.ts`; `bun test src/core/services/tokenizer.test.ts`; `bun run typecheck`; `bun run format:check:src`; `bun run changelog:lint`; `bun run test:fast`; `bun run test:env`; `bun run build`; `bun run test:smoke:dist`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,53 @@
---
id: TASK-334
title: Assess and address PR 57 latest CodeRabbit comments
status: Done
assignee:
- '@codex'
created_date: '2026-05-04 05:03'
updated_date: '2026-05-04 05:07'
labels:
- pr-feedback
- coderabbit
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/57'
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Assess the latest CodeRabbit review on PR #57 submitted 2026-05-04 and fix verified issues. Current scope: AniList post-watch duplicate-write race, known-word cache mutation return value, and manual-mark AniList rejection isolation with regression coverage.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Each latest CodeRabbit comment is either fixed or explicitly assessed as not applicable against current code.
- [x] #2 Regression tests cover behavior changes where practical.
- [x] #3 Relevant focused tests and typecheck pass, or any blocked verification is documented.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Verify each latest CodeRabbit finding against current code.
2. Update known-word cache append return semantics so cache clears are reported as mutations when state existed.
3. Acquire AniList post-watch in-flight before async gating and release in finally.
4. Isolate manual-mark AniList callback failures in IPC and add a rejection-path regression test.
5. Run focused tests for touched areas plus typecheck; document any blocked verification.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Verified latest CodeRabbit review submitted 2026-05-04 on PR #57. Fixed all three current items: known-word cache mutation return after cache reset, AniList post-watch concurrent in-flight race, and manual watched mark isolation from AniList callback failures. Added regression tests for each path and a changelog fragment.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed latest PR #57 CodeRabbit feedback by reporting known-word cache clears as mutations during immediate append, acquiring AniList post-watch in-flight before awaited gates to prevent duplicate writes, and isolating manual watched mark success from AniList post-watch callback failures. Added focused regression coverage in known-word cache, AniList post-watch, and IPC tests, plus a changelog fragment.
Verification: bun test src/anki-integration/known-word-cache.test.ts; bun test src/main/runtime/anilist-post-watch.test.ts; bun test src/core/services/ipc.test.ts; bun run typecheck; bun run format:check:src; bun run changelog:lint; bun run test:fast; bun run test:env; bun run build; bun run test:smoke:dist.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,39 @@
---
id: TASK-335
title: Fix Linux AniList setup gate using stored keyring token
status: Done
assignee: []
created_date: '2026-05-04 05:26'
updated_date: '2026-05-04 05:30'
labels:
- anilist
- bug
- linux
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
AniList setup page reopens on Linux video launch even when the token exists in secret storage and post-watch updates can use it. Investigate setup gating versus update token refresh paths and make them agree on stored-token availability.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launching a video on Linux with an AniList token available in secret storage does not show the AniList setup page just because config accessToken is empty.
- [x] #2 If secret storage load fails, setup/errors surface the underlying storage problem instead of behaving like an empty token.
- [x] #3 Regression coverage exercises the setup-gate token availability path and preserves post-watch update token behavior.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Patched AniList setup callback to require successful token persistence before caching/closing the setup flow. Patched config reload auth refresh to pass allowSetupPrompt:false so normal startup/playback reloads do not open AniList setup UI. Added regression coverage around persistence failure and non-prompting config refresh.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed AniList setup/login flow so failed encrypted token persistence no longer reports success or seeds only an in-memory token. Config reload now refreshes AniList auth state without opening the setup window during playback, reducing repeated Linux setup prompts when safeStorage/keyring resolution fails.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,39 @@
---
id: TASK-337
title: Fix transient Linux safeStorage failure poisoning AniList token store
status: Done
assignee: []
created_date: '2026-05-04 05:51'
updated_date: '2026-05-04 05:52'
labels:
- anilist
- bug
- linux
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
AniList token store memoizes a false safeStorage availability result. On Linux this can happen before Electron/keyring readiness, causing later post-watch updates and setup saves to report missing login/encryption unavailable even after the keyring is available.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A transient safeStorage unavailable result does not prevent a later stored AniList token load once encryption is available.
- [x] #2 A transient safeStorage unavailable result does not prevent a later AniList token save once encryption is available.
- [x] #3 Regression coverage protects the retry behavior.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Changed AniList token store safeStorage probe to memoize successful probes only. Failed probes now return false without poisoning later load/save attempts, covering Linux startup windows where Electron safeStorage/keyring can be unavailable before app readiness but usable later. Added regression test for transient unavailable -> available load/save retry.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed a Linux AniList auth failure where an early safeStorage/keyring miss was cached for the whole process. Stored tokens now load and setup tokens can save after GNOME libsecret becomes available later in startup.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,72 @@
---
id: TASK-338
title: Fix known-word highlight on standalone subtitle particles
status: Done
assignee:
- codex
created_date: '2026-05-04 05:52'
updated_date: '2026-05-04 05:57'
labels:
- bug
- subtitle
- tokenizer
dependencies: []
references:
- src/core/services/tokenizer/annotation-stage.ts
- src/core/services/tokenizer/subtitle-annotation-filter.ts
- src/renderer/subtitle-render.ts
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Standalone grammar particles such as に should not render as known-word green when they appear in the known-word cache as readings for other words. Keep known-word coloring for lexical tokens, but prevent grammar-excluded subtitle tokens from getting known-green.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Standalone grammar particles like に do not retain isKnown after subtitle annotation filtering.
- [x] #2 Lexical known-word tokens still render as known when not grammar-excluded.
- [x] #3 Focused regression test covers the particle false-positive path.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression in `src/core/services/tokenizer/annotation-stage.test.ts` showing standalone particle `に` is grammar-excluded and does not retain `isKnown` even when `isKnownWord('に')` is true.
2. Run the focused tokenizer annotation test and confirm the new test fails for the current behavior.
3. Patch `src/core/services/tokenizer/annotation-stage.ts` so grammar-excluded tokens clear known status while still stripping N+1/frequency/JLPT/name metadata.
4. Run the focused test file, then inspect diff and update task acceptance criteria.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented tokenizer annotation filtering so grammar-excluded subtitle tokens clear known-word status instead of retaining green known coloring. Added focused regression for known-word-cache particle false positive and updated existing expectations for unified annotation clearing. Verification: `bun test src/core/services/tokenizer/annotation-stage.test.ts --test-name-pattern "clears known status from standalone particles"` failed before the production patch; after patch, `bun test src/core/services/tokenizer/annotation-stage.test.ts`, `bun test src/core/services/tokenizer.test.ts`, combined tokenizer tests, `bun run typecheck`, `bun run changelog:lint`, and `bun run test:fast` passed.
Full handoff gate follow-up: `bun run test:env` and `bun run build` passed. `bun run test:smoke:dist` failed outside this tokenizer change in `dist/core/services/overlay-manager.test.js` because current dirty overlay-window code calls `window.getTitle()` on a test mock that does not provide it.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Cleared `isKnown` for grammar-excluded subtitle tokens in the tokenizer annotation stage, preventing standalone particles such as `に` from rendering as known just because a known-word deck contains a matching reading.
- Added a focused regression test for the known-word-cache false positive and updated tokenizer expectations so helper/grammar spans consistently clear all subtitle annotations.
- Added changelog fragment `changes/338-known-word-particle-highlights.md`.
Verification:
- `bun test src/core/services/tokenizer/annotation-stage.test.ts --test-name-pattern "clears known status from standalone particles"` failed before the production patch.
- `bun test src/core/services/tokenizer/annotation-stage.test.ts`
- `bun test src/core/services/tokenizer.test.ts`
- `bun test src/core/services/tokenizer/annotation-stage.test.ts src/core/services/tokenizer.test.ts`
- `bun run typecheck`
- `bun run changelog:lint`
- `bun run test:fast`
- `bun run test:env`
- `bun run build`
Blocked/External:
- `bun run test:smoke:dist` currently fails outside this tokenizer change in `dist/core/services/overlay-manager.test.js`: dirty overlay-window code calls `window.getTitle()` on a test mock without that method.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,65 @@
---
id: TASK-340
title: Restore default replay and next subtitle overlay keybindings
status: Done
assignee:
- Codex
created_date: '2026-05-04 06:25'
updated_date: '2026-05-04 06:49'
labels:
- bug
- keybindings
- overlay
- mpv
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Default overlay/mpv keybindings for replaying the current subtitle line and playing the next subtitle line are not firing. Shift+H and Shift+L subtitle jumps still work, but Ctrl+Shift+H should replay the current subtitle and pause at subtitle end, and Ctrl+Shift+L should play the next subtitle and pause at subtitle end. Keep the other built-in defaults working.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Default keybindings include working replay-current-subtitle and play-next-subtitle bindings on Ctrl+Shift+H and Ctrl+Shift+L.
- [x] #2 Replay-current-subtitle dispatch reaches the existing runtime path that pauses at the subtitle end.
- [x] #3 Play-next-subtitle dispatch reaches the existing runtime path that pauses at the subtitle end.
- [x] #4 Existing default keybindings continue to compile/register without regressions.
- [x] #5 Focused regression tests cover the broken default bindings.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused regression coverage that the resolved defaults compile on Linux without dropping Ctrl+Shift+H/L, and that those keys map to replayCurrentSubtitle/playNextSubtitle session actions.
2. Move the default session-help shortcut off Ctrl/Cmd+Shift+H to a non-conflicting shortcut, then update generated/default config docs so shipped defaults match documentation.
3. Add/adjust coverage for default replay/next bindings and run targeted Bun tests plus plugin session-binding smoke.
4. Follow-up after live test: fix the mpv plugin shifted-letter key-name conversion so `Ctrl+Shift+KeyL` registers using mpv's uppercase letter form and add Lua regression coverage for both `Ctrl+Shift+L` and `Shift+L`.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Root cause: default `shortcuts.openSessionHelp = CommandOrControl+Shift+H` canonicalized to `ctrl+shift+KeyH` on Linux/Windows, conflicting with the built-in replay-current-subtitle keybinding. The session-binding compiler drops conflicted bindings, so replay did not register. Moved default session help to `CommandOrControl+Slash` and added regression coverage that defaults compile without a conflict and keep replay/next actions on `Ctrl+Shift+H/L`.
Follow-up from live test: `Ctrl+Shift+H` works after resolving the help shortcut conflict, but `Ctrl+Shift+L` still behaves like native/other `Ctrl+L`. Investigating mpv/plugin key-name generation for shifted letter chords.
Follow-up fix: mpv normalizes shifted letter chords to uppercase letter key names (for example `Ctrl+Shift+l` becomes `Ctrl+L`). The plugin previously emitted `Ctrl+Shift+l`, which let live `Ctrl+Shift+L` fall through as the `Ctrl+L` key path. `plugin/subminer/session_bindings.lua` now emits uppercase letters and omits the Shift modifier for shifted `Key[A-Z]` bindings. Lua regression coverage now checks `Ctrl+Shift+KeyL -> Ctrl+L`, `Shift+KeyL -> L`, and the play-next CLI dispatch.
Second live follow-up: `Ctrl+Shift+L` routed to play-next but still behaved like `Shift+L` when playback was already paused because `MpvIpcClient.playNextSubtitle()` explicitly cleared `pendingPauseAtSubEnd` and only sent `sub-seek 1` in paused state. Changed play-next to always arm pause-at-sub-end, clear stale pause target, seek to next subtitle, and unpause when currently paused. Existing sub-end/time-pos handling then pauses at the next subtitle end.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Changed the default session-help shortcut from `CommandOrControl+Shift+H` to `CommandOrControl+Slash` so `Ctrl+Shift+H` remains available for replay-current-subtitle and `Ctrl+Shift+L` remains available for play-next-subtitle. Updated config examples, docs-site shortcut/config/usage docs, and added changelog fragment `changes/340-default-subtitle-keybindings.md`.
Fixed both follow-up issues from live testing. First, the mpv plugin key-name converter now uses mpv's uppercase key form for shifted letter bindings (`Ctrl+Shift+KeyL` registers as `Ctrl+L`, `Shift+KeyL` as `L`). Second, `MpvIpcClient.playNextSubtitle()` now starts playback even when mpv is paused, keeps the pause-at-sub-end path armed, and lets existing subtitle-end timing pause again at the next subtitle end.
Regression coverage now includes compiled default bindings, Lua plugin shifted-letter registration/CLI dispatch, and paused-state play-next behavior.
Verification passed: targeted Bun session/mpv/protocol tests, `bun run test:plugin:src`, `bun run changelog:lint`, `bun run build`, and `bun run test:smoke:dist`. Earlier full gate also passed before the follow-ups: `bun run typecheck`, `bun run test:fast`, `bun run test:env`, docs/config checks, and dist smoke.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,66 @@
---
id: TASK-341
title: Fix frequency highlight for honorific prefix noun tokens
status: Done
assignee:
- codex
created_date: '2026-05-05 02:08'
updated_date: '2026-05-05 02:10'
labels:
- bug
- tokenizer
- frequency
dependencies: []
documentation:
- docs/architecture/2026-03-15-renderer-performance-design.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
User reported subtitle token `ご機嫌` in `(フランク)ご機嫌が良くないようだな アンドリュー` shows Yomitan/JPDB rank 5484 in popup but is not highlighted as frequent. Frequency annotation currently excludes merged tokens containing default-excluded POS parts such as `接頭詞`; ordinal prefix-noun tokens already have an exception. Desired outcome: honorific prefix + noun lexical tokens like `ご機嫌` keep their valid frequency rank so renderer can apply frequent-token styling, while standalone prefixes and noisy merged grammar fragments remain excluded.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `ご機嫌`-style honorific prefix + noun tokens retain a finite frequency rank after annotation/tokenization when frequency highlighting is enabled.
- [x] #2 Standalone prefix/noise tokens remain excluded from frequency annotation.
- [x] #3 Regression test covers the reported `ご機嫌` rank 5484 behavior.
- [x] #4 Relevant tokenizer/annotation tests pass.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a failing regression around honorific prefix + noun token frequency retention, using `ご機嫌` with rank 5484 and POS `接頭詞|名詞` / `名詞接続|一般`.
2. Implement a narrow annotation-stage exception for lexical honorific prefix-noun tokens, adjacent to the existing ordinal prefix-noun allowance.
3. Verify standalone prefix/noise exclusion behavior remains covered.
4. Run targeted tokenizer/annotation tests and update acceptance criteria/final notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
TDD red verified: `bun test src/core/services/tokenizer.test.ts -t "honorific prefix-noun"` failed with `actual: undefined`, `expected: 5484` before implementation.
Implemented a narrow honorific prefix-noun frequency allowance for merged `お`/`ご`/`御` + noun tokens with POS `接頭詞|名詞` and prefix POS2 `名詞接続`. Existing standalone prefix/noise exclusion tests still pass.
Verification: `bun test src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts` passed (164 tests); `bun run typecheck` passed; `bunx prettier --check src/core/services/tokenizer/annotation-stage.ts src/core/services/tokenizer.test.ts` passed. Repo-wide `bun run format:check:src` still fails on pre-existing `src/core/services/stats-window.ts` formatting.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed frequency annotation for lexical honorific prefix-noun tokens such as `ご機嫌`. The annotation filter now allows merged `お`/`ご`/`御` prefix + noun tokens with MeCab POS `接頭詞|名詞` / `名詞接続|...` to retain a valid frequency rank, while standalone prefixes and existing noise filters remain excluded.
Added a tokenizer regression for the reported `ご機嫌` case asserting rank `5484` is preserved after MeCab enrichment and annotation.
Verification:
- `bun test src/core/services/tokenizer.test.ts -t "honorific prefix-noun"` failed before the fix with `undefined` vs `5484`, then passed after the fix.
- `bun test src/core/services/tokenizer.test.ts src/core/services/tokenizer/annotation-stage.test.ts` passed (164 tests).
- `bun run typecheck` passed.
- `bunx prettier --check src/core/services/tokenizer/annotation-stage.ts src/core/services/tokenizer.test.ts` passed.
Note: repo-wide `bun run format:check:src` currently fails on unrelated existing formatting in `src/core/services/stats-window.ts`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,54 @@
---
id: TASK-342
title: Improve docs homepage indexability
status: Done
assignee:
- codex
created_date: '2026-05-11 04:53'
updated_date: '2026-05-11 05:06'
labels:
- docs
- seo
dependencies: []
references:
- 'https://docs.subminer.moe/'
- >-
https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Google Search Console reports https://docs.subminer.moe/ as Crawled - currently not indexed. Investigate and improve the docs-site homepage and crawl signals so the root docs page has clear unique content, self-consistent canonical metadata, and no avoidable duplicate sitemap entry from VitePress README output. Keep the change scoped to docs-site SEO/content and verify with docs tests/build plus live-style generated HTML inspection.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Generated docs HTML for https://docs.subminer.moe/ declares a self-referential canonical URL and does not declare noindex.
- [x] #2 Generated sitemap does not advertise a duplicate /README docs page when the docs homepage is the intended canonical root.
- [x] #3 Docs tests cover the SEO/indexing signals so canonical and duplicate sitemap regressions are caught.
- [x] #4 Docs build/test commands pass or any blocker is documented with exact failure output.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add failing docs-site tests for canonical head generation and sitemap duplicate filtering. 2. Update VitePress config to emit canonical URLs and exclude /README from the sitemap. 3. Strengthen docs homepage content with unique overview/decision-path copy. 4. Run docs tests/build and inspect generated HTML/sitemap.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
2026-05-11: Added docs SEO tests, root canonical generation, sitemap README filtering, and stronger docs homepage orientation copy. Verified generated index.html contains self canonical/no noindex and generated sitemap omits /README.
2026-05-11: Added changes/342-docs-indexability.md and verified changelog lint. Final verification: bun run --cwd docs-site test, bun run docs:build, bun x prettier --check touched docs/changelog files, bun run changelog:lint.
2026-05-11: User requested removal of the added homepage orientation block. Removed it, removed the matching content test, and updated the changelog fragment. Generated index.html no longer contains the removed headings while preserving canonical metadata.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Improved docs homepage indexability signals for https://docs.subminer.moe/. Added self-referential canonical generation for VitePress pages and filtered the duplicate /README page out of the generated sitemap. Added docs SEO regression coverage, kept the existing Plausible hostname test aligned with the shared hostname constant, and added a docs changelog fragment. Per follow-up request, the extra homepage orientation copy was removed before handoff. Verification passed: docs tests, docs build, targeted generated HTML/sitemap inspection, Prettier check for touched files, and changelog lint.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,44 @@
---
id: TASK-343
title: Fix macOS character dictionary selector session shortcut
status: Done
assignee: []
created_date: '2026-05-11 08:05'
updated_date: '2026-05-11 08:06'
labels:
- bug
- macos
- character-dictionary
- plugin
dependencies: []
modified_files:
- plugin/subminer/session_bindings.lua
- scripts/test-plugin-session-bindings.lua
priority: medium
ordinal: 181500
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Opening the character dictionary AniList selector from mpv/session shortcuts should work on macOS. Current generated session bindings include the openCharacterDictionary session action, but the Lua plugin CLI dispatch table does not map that action to the app flag, so the shortcut cannot reach the runtime selector.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The openCharacterDictionary session action invokes the app with --open-character-dictionary from the mpv plugin.
- [x] #2 Regression coverage proves the Lua session-binding CLI map forwards openCharacterDictionary correctly.
- [x] #3 Existing session-binding regression coverage still passes.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
TDD red: `lua scripts/test-plugin-session-bindings.lua` failed because openCharacterDictionary did not emit --open-character-dictionary. Green after adding the missing Lua CLI mapping.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed the mpv plugin session action mapping so the character dictionary selector shortcut dispatches `--open-character-dictionary` to the app. Added Lua regression coverage for the macOS-style Alt+Meta+A binding and verified adjacent TypeScript session binding tests.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,74 @@
---
id: TASK-344
title: Fix macOS overlay tracker hiding while mpv remains active
status: Done
assignee:
- codex
created_date: '2026-05-11 08:27'
updated_date: '2026-05-11 08:41'
labels:
- bug
- macos
- overlay
dependencies: []
references:
- src/main/runtime/overlay-visibility-runtime.ts
- src/window-trackers
modified_files:
- src/core/services/overlay-visibility.ts
- src/core/services/overlay-visibility.test.ts
- changes/344-macos-overlay-active-mpv.md
priority: high
ordinal: 182500
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
macOS playback overlay should match Windows behavior: the tracker may only hide or alter overlay layering when mpv is no longer the active playback window. When mpv remains topmost or fullscreen, the visible overlay must stay present and interactive unless the user manually hides it or minimizes mpv.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 With mpv active on macOS, window-tracker updates do not hide the visible overlay or make it click-through.
- [x] #2 With mpv fullscreen on macOS, tracker geometry/layering refreshes preserve overlay interactivity.
- [x] #3 Overlay visibility still changes when mpv is no longer active, and manual hide/minimize behavior remains intact.
- [x] #4 A regression test covers the macOS active-mpv path that previously produced an overlay loading OSD and non-interactive overlay.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression in `src/core/services/overlay-visibility.test.ts` for macOS with mpv tracked/focused and retained geometry: visibility refresh must not hide the overlay, must not emit loading OSD, and must leave the overlay interactive (`setIgnoreMouseEvents(false)`) unless forced passthrough is active.
2. Update `src/core/services/overlay-visibility.ts` so macOS tracker refreshes preserve the visible overlay while mpv is active/fullscreen, and only hide/re-layer for explicit manual hide, minimized/untracked target, or non-active mpv cases.
3. Run the focused overlay visibility tests, then a broader fast gate if the focused fix is green.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented in the shared overlay visibility service. macOS now keeps tracked/focused mpv overlays interactive instead of defaulting to mouse passthrough, and preserves an already visible active-mpv overlay during temporary tracker-not-ready refreshes without showing the loading OSD. Forced passthrough, modal hide, manual hide, Windows minimized handling, and initial macOS tracker-not-ready startup behavior remain covered by tests.
Verification: `bun test src/core/services/overlay-visibility.test.ts` passed; affected overlay/mouse/runtime group passed; `bun run typecheck`, `bun run changelog:lint`, `bun run build`, `bun run test:env`, and `bun run test:smoke:dist` passed. `bun run test:fast` is blocked by existing cross-file test pollution: `src/core/services/subsync.test.ts` passes alone, but fails when run after `src/renderer/handlers/keyboard.test.ts` because `window.electronAPI` is undefined in a lingering keyboard handler; Bun then reports nested node:test errors for later files. `bun run format:check:src` is blocked by pre-existing formatting drift in `src/core/services/stats-window.ts`; touched files pass direct Prettier check.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Updated macOS overlay visibility logic so a tracked/focused mpv window keeps the visible overlay interactive instead of click-through.
- Preserved an already visible active-mpv overlay during temporary macOS tracker-not-ready refreshes, avoiding the loading OSD/hide path for that active playback case.
- Added regression coverage for active mpv tracker refreshes and transient tracker-not-ready refreshes, plus updated old macOS expectations to the new active-mpv contract.
- Added a changelog fragment for the user-visible overlay fix.
Verification:
- Passed: `bun test src/core/services/overlay-visibility.test.ts`
- Passed: `bun test src/core/services/overlay-visibility.test.ts src/core/services/overlay-runtime-init.test.ts src/core/services/overlay-shortcut-handler.test.ts src/renderer/handlers/mouse.test.ts`
- Passed: `bun run typecheck`
- Passed: `bun run changelog:lint`
- Passed: `bun run build`
- Passed: `bun run test:env`
- Passed: `bun run test:smoke:dist`
- Blocked: `bun run test:fast` by existing keyboard/subsync cross-file global pollution; isolated `bun test src/core/services/subsync.test.ts` passes.
- Blocked: `bun run format:check:src` by pre-existing formatting drift in `src/core/services/stats-window.ts`; touched files pass direct Prettier check.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,70 @@
---
id: TASK-345
title: Address PR 57 latest CodeRabbit review comments
status: Done
assignee:
- codex
created_date: '2026-05-12 06:35'
updated_date: '2026-05-12 06:38'
labels:
- pr-review
- coderabbit
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/57'
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Assess the 2026-05-11 CodeRabbit review on PR #57 and address still-valid actionable comments with minimal changes. Current comments cover mpv subtitle playback unpause behavior, parser-selection empty reading handling, and overlay focus selector accessibility.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Still-valid CodeRabbit comments from the latest PR #57 review are fixed or explicitly documented as skipped with rationale.
- [x] #2 Regression coverage is added for behavior-affecting fixes where practical before production code changes.
- [x] #3 Relevant targeted checks pass locally.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect current code and nearby tests for the three latest CodeRabbit comments on PR #57.
2. Add regression tests first for behavior-impacting findings: mpv playNextSubtitle unpauses when pause state is unknown, and parser selection preserves combined reading for empty segment readings.
3. Apply minimal production fixes plus the CSS :focus-visible selector change.
4. Run targeted test commands for touched areas and update task notes/final status.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Added red tests for the two behavior comments. `bun test src/core/services/mpv.test.ts` fails because playNextSubtitle skips unpause when pause state is null. `bun test src/core/services/tokenizer/parser-selection-stage.test.ts` fails because empty grammar-ending reading clears preceding combined reading.
Implemented all three latest CodeRabbit findings. Added regressions for mpv unknown pause state and parser-selection empty reading handling; changed overlay focus selectors to :focus-visible. Also fixed existing Prettier failure in src/core/services/stats-window.ts. Verification passed: targeted tests, typecheck, test:fast, test:env, format:check:src, build, test:smoke:dist, changelog:lint, changelog:pr-check.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Addressed the latest CodeRabbit comments on PR #57.
Changes:
- `playNextSubtitle` now sends `pause=false` unless mpv is explicitly known to be playing, covering startup/reconnect unknown pause state.
- Parser selection no longer slices combined readings for empty grammar-ending readings, preserving preceding token readings.
- Overlay focus suppression now targets `:focus-visible` selectors.
- Applied Prettier to `src/core/services/stats-window.ts` to clear the existing formatting gate failure.
Verification:
- `bun test src/core/services/mpv.test.ts`
- `bun test src/core/services/tokenizer/parser-selection-stage.test.ts`
- `bun run typecheck`
- `bun run test:fast`
- `bun run test:env`
- `bun run format:check:src`
- `bun run build`
- `bun run test:smoke:dist`
- `bun run changelog:lint`
- `bun run changelog:pr-check`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,57 @@
---
id: TASK-346
title: Fix stats session detail when recent media is missing
status: Done
assignee:
- Codex
created_date: '2026-05-12 06:41'
updated_date: '2026-05-12 06:44'
labels:
- bug
- stats
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Stats overview can show a completed session, but clicking it opens a detail view that says "Media not found". The details view should resolve the session/media consistently for recently completed local playback so users can inspect session cards, words, timeline, and media stats after a video finishes.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A completed session listed on the stats overview opens a usable details view instead of "Media not found" when its media is still present in stats data.
- [x] #2 Regression coverage reproduces the overview-to-detail lookup mismatch and verifies the corrected behavior.
- [x] #3 Relevant stats/detail documentation is updated if behavior or APIs change.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a focused regression test in `src/core/services/immersion-tracker/__tests__/query.test.ts` covering a video/session visible from session summaries before `imm_lifetime_media` exists.
2. Update `getMediaDetail` in `src/core/services/immersion-tracker/query-library.ts` so detail rows can resolve from `imm_videos` plus session metrics when lifetime summary is absent.
3. Run the focused query test lane and update task notes/acceptance criteria.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented root-cause fix in `getMediaDetail`: media detail now resolves from `imm_videos` plus session metrics when `imm_lifetime_media` is not populated yet, while still preferring lifetime summary totals when available. Added regression test for session-visible/media-detail-missing mismatch. No docs update required because the API shape is unchanged; added changelog fragment `changes/346-stats-session-detail.md`. Verification: `bun test src/core/services/immersion-tracker/__tests__/query.test.ts`, `bun run typecheck`, `bun run format:check:src`, `bun run test:fast`, `bun run changelog:lint`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Fixed stats media detail lookup so sessions visible in overview can open detail even before lifetime media summaries exist.
- Preserved lifetime-summary totals when available and added a regression test for the missing-lifetime case.
- Added `changes/346-stats-session-detail.md` for the user-visible fix.
Tests:
- `bun test src/core/services/immersion-tracker/__tests__/query.test.ts`
- `bun run typecheck`
- `bun run format:check:src`
- `bun run test:fast`
- `bun run changelog:lint`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,61 @@
---
id: TASK-347
title: Address PR 57 CodeRabbit review round after stats session fix
status: Done
assignee:
- codex
created_date: '2026-05-12 07:02'
updated_date: '2026-05-12 09:48'
labels:
- pr-review
- coderabbit
- ci
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/57'
modified_files:
- src/core/services/overlay-window-input.ts
- src/core/services/overlay-window.test.ts
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Assess and address the 2026-05-12 CodeRabbit review on PR #57 plus the current red GitHub Actions check. Latest comments cover stats session detail token aggregation, Linux fullscreen overlay refresh scheduling, Hyprland title-event polling, malformed Hyprland monitor JSON handling, and JLPT-lock test coverage for name matches.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Still-valid latest CodeRabbit findings on PR #57 are fixed or documented as skipped with rationale.
- [x] #2 CI failure context is inspected and any repo-relevant failing tests or formatting issues are fixed.
- [x] #3 Regression coverage is added for behavior changes where practical before production edits.
- [x] #4 Relevant local verification passes.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Inspect failing GitHub Actions log and current code around each latest CodeRabbit finding.
2. Add or update focused regression tests first for behavior changes: stats token aggregation, fullscreen refresh exit cancellation, Hyprland monitor parse failure, and title-only event filtering.
3. Apply minimal production fixes for still-valid findings, plus the subtitle-render duplicate test coverage item.
4. Run targeted tests first, then format/typecheck and broader relevant gates; update the task with results.
Address latest CodeRabbit callback-scope finding: add regression coverage that macOS visible-overlay blur does not invoke onWindowsVisibleOverlayBlur, then split win32/darwin blur handling in src/core/services/overlay-window-input.ts.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
2026-05-12 09:47: Assessed latest PR #57 CodeRabbit comments via gh. Prior review findings in comments are marked addressed by CodeRabbit; latest still-actionable item was macOS visible-overlay blur invoking the Windows-only blur callback in overlay-window-input.ts.
Added regression coverage showing macOS visible-overlay blur leaves onWindowsVisibleOverlayBlur inactive; test failed before production change and passed after splitting win32/darwin handling.
Verification: bun test src/core/services/overlay-window.test.ts; bun run typecheck; git diff --check. PR checks: build-test-audit pass, GitGuardian pass, CodeRabbit pass/skipped, Claude skipped.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Addressed the latest CodeRabbit callback-scope review on PR #57. Windows visible-overlay blur still invokes onWindowsVisibleOverlayBlur and returns without restacking; macOS visible-overlay blur now returns without restacking and without invoking the Windows-only callback. Added focused regression coverage and verified targeted tests, typecheck, diff whitespace, and current PR checks.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,56 @@
---
id: TASK-348
title: Fix PR 57 coverage CI focus chrome failure
status: Done
assignee:
- '@codex'
created_date: '2026-05-12 07:02'
updated_date: '2026-05-12 07:11'
labels:
- ci
- bug
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/pull/57'
- 'https://github.com/ksyasuda/SubMiner/actions/runs/25718536412'
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Investigate and fix current GitHub Actions `build-test-audit` failure on PR #57 (`tokenizer-updates`). CI fails during `bun run test:coverage:src` in the maintained source lane: `renderer stylesheet hides focus chrome on top-level overlay focus targets`.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Root cause of the focus chrome coverage failure is identified from CI/local test output.
- [x] #2 A focused fix is applied without broad unrelated changes.
- [x] #3 Relevant local coverage/test command passes.
- [x] #4 Remote PR check status is rechecked or next CI action is documented.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Reproduce the CI failure locally with `bun test src/renderer/overlay-legacy-cleanup.test.ts`.
2. Update the stale legacy cleanup assertion to expect top-level `:focus-visible` suppression and reject broad `:focus` suppression.
3. Run the targeted test and `bun run test:coverage:src` to match CI's failing lane.
4. Recheck PR checks or document that CI needs a push/rerun.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
CI/local root cause: `src/renderer/style.css` was intentionally changed to `html/body/#overlay:focus-visible`, but `src/renderer/overlay-legacy-cleanup.test.ts` still required broad `:focus` selectors. The stale assertion fails in `test:coverage:src`.
Additional coverage-lane failure after first fix: `src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts` imported `updateLinuxMpvFullscreenOverlayRefreshBurst`, but `src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts` did not export/implement it. Added the helper to cancel existing bursts and schedule only while fullscreen is true.
Verification passed: `bun test src/renderer/overlay-legacy-cleanup.test.ts`; `bun test src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts`; `bun run test:coverage:src`; `bun run format:check:src`. `gh pr checks 57` still reports the old failed `build-test-audit` run at run 25718536412; branch needs push/rerun for remote green.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed current PR #57 `build-test-audit` CI blockers. Updated the stale overlay legacy cleanup assertion to expect `:focus-visible` top-level focus suppression and guard against reintroducing broad `:focus` suppression. Added the missing `updateLinuxMpvFullscreenOverlayRefreshBurst` export used by the Linux fullscreen overlay refresh tests. Verification passed locally: focused overlay legacy cleanup test, focused Linux fullscreen refresh test, `bun run test:coverage:src`, and `bun run format:check:src`. Remote PR checks still show the old failed `build-test-audit` run until these local changes are pushed and CI reruns.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,74 @@
---
id: TASK-349
title: Fix macOS overlay window ordering behind foreground apps
status: Done
assignee: []
created_date: '2026-05-12 08:50'
updated_date: '2026-05-12 08:58'
labels:
- bug
- macos
- overlay
dependencies: []
references:
- src/core/services/overlay-visibility.ts
- src/window-trackers
- TASK-344
modified_files:
- src/core/services/overlay-visibility.ts
- src/core/services/overlay-visibility.test.ts
- src/core/services/overlay-window-input.ts
- src/core/services/overlay-window.test.ts
- changes/349-macos-overlay-z-order.md
priority: high
ordinal: 183500
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
macOS overlay should stay visually above mpv, but remain grouped with mpv in normal desktop stacking. When another app/window is brought in front of mpv, that window must also appear in front of the overlay, matching Windows behavior. This follows the earlier active-mpv fix that stopped the overlay from hiding while mpv remained foremost.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 When mpv is the foreground playback window on macOS, the overlay remains visible above mpv.
- [x] #2 When another application or window is brought in front of mpv on macOS, that foreground window appears above both mpv and the overlay.
- [x] #3 Restoring mpv to the foreground restores the overlay above mpv without requiring a restart.
- [x] #4 Regression coverage documents the macOS stacking relationship and does not regress the prior active-mpv overlay preservation behavior.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused regression coverage for macOS mpv focus loss: the overlay must release its topmost level, remain visible/click-through, and stop enforcing layer order while mpv is behind another window.
2. Add focused blur-handler coverage so the macOS visible overlay does not restack itself when it loses focus.
3. Update overlay visibility and blur handling to use tracker focus as the macOS stacking boundary: focused mpv raises overlay; unfocused mpv releases topmost and skips restack.
4. Run focused overlay tests, formatting, typecheck, changelog lint, env/build/smoke checks; document any blocked broad gate separately.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented macOS stacking boundary using tracker focus. When tracked mpv is unfocused and the overlay itself is not focused, the visible overlay now releases Electron always-on-top, remains visible/click-through, and skips layer-order enforcement. Visible overlay blur restacking is also skipped on macOS, matching the Windows no-restack path for focus loss. `test:fast` remains blocked by existing cross-file pollution: `keyboard.test.ts` leaves `window.electronAPI` undefined for a later `subsync.test.ts`, causing Bun nested `node:test` errors in subsequent files.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Updated macOS overlay visibility so tracked mpv focus controls stacking: focused mpv keeps the overlay raised; unfocused mpv releases topmost while keeping the overlay visible and click-through.
- Stopped macOS visible overlay blur handling from immediately restacking the overlay above unrelated foreground windows.
- Added regression tests for macOS mpv focus loss and macOS blur restacking behavior.
- Added a changelog fragment for the user-visible overlay z-order fix.
Verification:
- Passed: `bun test src/core/services/overlay-visibility.test.ts src/core/services/overlay-window.test.ts`
- Passed: `bunx prettier --check src/core/services/overlay-visibility.ts src/core/services/overlay-visibility.test.ts src/core/services/overlay-window-input.ts src/core/services/overlay-window.test.ts changes/349-macos-overlay-z-order.md`
- Passed: `bun run typecheck`
- Passed: `bun run changelog:lint`
- Passed: `bun run test:env`
- Passed: `bun run build`
- Passed: `bun run test:smoke:dist`
- Blocked: `bun run test:fast` by existing keyboard/subsync cross-file global pollution; focused and environment tests pass.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,62 @@
---
id: TASK-350
title: Fix known highlighting for Yomitan compound tokens
status: Done
assignee:
- codex
created_date: '2026-05-12 09:08'
updated_date: '2026-05-12 09:29'
labels:
- bug
- tokenizer
dependencies: []
modified_files:
- src/core/services/tokenizer/yomitan-parser-runtime.ts
- src/core/services/tokenizer/yomitan-parser-runtime.test.ts
- src/core/services/tokenizer.test.ts
- changes/350-known-yomitan-token-highlighting.md
priority: high
ordinal: 184500
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Subtitle known-word coloring should respect the lexical token selected by Yomitan. If Yomitan emits a compound or inflected expression as one token, SubMiner must not mark that displayed token known solely because MeCab/POS enrichment can decompose it into known component words.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 A Yomitan token such as `取り組んで` with headword `取り組む` remains not-known when only component words like `取る` or `組む` are known.
- [x] #2 Frequency/JLPT/POS enrichment still works for the selected Yomitan token without leaking component known-word status into `isKnown`.
- [x] #3 Regression coverage demonstrates the compound-token case and fails on current behavior before the fix.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a regression in `src/core/services/tokenizer.test.ts` for a Yomitan-selected compound token: Yomitan emits `取り組んで` with headword `取り組む`; MeCab splits the same span into component tokens whose headwords include known component words such as `組む`; expected result is one displayed token with `isKnown === false` when only the components are known.
2. Verify the regression fails on current code.
3. Patch MeCab enrichment so it only contributes POS metadata used by annotation filters/exclusions. It must preserve the Yomitan token's `surface`, `headword`, `reading`, offsets, and existing lexical annotation state, especially `isKnown`.
4. Re-run the targeted tokenizer test, then a relevant fast test lane if practical.
After inspecting code, MeCab enrichment currently only writes POS metadata. The observed component coloring can also come from SubMiner's custom Yomitan scanning path fragmenting a phrase differently than Yomitan's internal parser. Regression should exercise `requestYomitanScanTokens` fallback/parser behavior as seen by `tokenizeSubtitle`, and the fix should prefer Yomitan internal parse token identity while keeping MeCab limited to filtering/POS metadata.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
User clarified MeCab is intended only to help filter unwanted characters/particles/sound effects/etc., not to alter lexical tokenization or known-word decisions.
Implementation settled on parse-first token identity: `requestYomitanScanTokens` now reads Yomitan internal parse tokens first. It still runs the scanner to keep scanner metadata when spans agree, but returns parse tokens when the scanner fragments the parse token. MeCab remains POS/filter enrichment only.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed known-word highlighting for Yomitan compound tokens by preferring Yomitan internal parse token spans over fragmented scanner output. When scanner output agrees with parse spans, scanner metadata such as name-match and word classes is preserved; when it fragments a Yomitan token, the parse token identity wins so known component words do not color the larger unknown token green.
Added regressions for `取り組んで` with known component words (`取る`, `組む`, `もらう`) and for parser-runtime token selection/metadata behavior. Added a changelog fragment.
Validation run: `bun test src/core/services/tokenizer.test.ts src/core/services/tokenizer/yomitan-parser-runtime.test.ts src/core/services/tokenizer/parser-selection-stage.test.ts src/core/services/tokenizer/parser-enrichment-stage.test.ts`; `bun run typecheck`; `bun x prettier --check src/core/services/tokenizer.test.ts src/core/services/tokenizer/yomitan-parser-runtime.ts src/core/services/tokenizer/yomitan-parser-runtime.test.ts changes/350-known-yomitan-token-highlighting.md`; `bun run changelog:lint`; `git diff --check`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,70 @@
---
id: TASK-357
title: Add primary subtitle bar visibility modes
status: Done
assignee:
- Codex
created_date: '2026-05-13 03:17'
updated_date: '2026-05-13 03:24'
labels:
- overlay
- subtitles
- config
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Update the primary subtitle bar visibility control so the `v` key cycles through the same mode model used by secondary subtitles: hidden, visible, and hover/auto. The primary and secondary subtitle bars must keep separate visibility state and config defaults, and primary visibility changes must identify the primary bar in OSD feedback.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Pressing `v` cycles primary subtitle bar visibility through hidden, visible, and hover/auto without changing secondary subtitle visibility.
- [x] #2 In hover/auto mode, the primary subtitle bar is normally hidden but becomes visible and interactable when hovering over its reserved location.
- [x] #3 Primary and secondary subtitle visibility modes have independent runtime state and config defaults.
- [x] #4 `subtitleStyle` exposes a primary subtitle visibility default that defaults to visible and validates invalid values with a warning/fallback.
- [x] #5 OSD feedback is shown when primary visibility mode changes and clearly identifies the primary subtitle bar.
- [x] #6 Relevant tests and config example/docs are updated.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add failing tests for primary subtitle mode config default/validation and renderer `v` cycling/OSD behavior.
2. Add primary subtitle mode type/config default under `subtitleStyle`, parse it with warning fallback, include it in generated example config.
3. Carry primary mode in renderer state/startup/hot-reload payloads independently from secondary mode.
4. Replace primary boolean toggle with `hidden -> visible -> hover` cycle and OSD text `Primary subtitle: <mode>`.
5. Add primary hover CSS mirroring secondary hover behavior so hover mode reserves a hit area and becomes interactable on hover.
6. Run focused tests, then broader relevant checks if time/environment allows.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented primary subtitle visibility as independent `PrimarySubMode` (`hidden | visible | hover`) with renderer state, startup default from `subtitleStyle.primaryDefaultMode`, hot-reload payload propagation, and primary-specific OSD. Focused tests were added before implementation and all focused tests passed. Full relevant gate passed: `bun run typecheck`, `bun run test:config`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`, `bun run docs:test`, `bun run docs:build`, `bun run format:check:src`, and `bun run changelog:lint`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added `subtitleStyle.primaryDefaultMode` with default `visible`, enum validation, generated config examples, and renderer/hot-reload payload wiring.
- Changed the primary subtitle bar `v` action from a boolean hide/show toggle to independent `hidden | visible | hover` mode cycling with OSD text that identifies the primary subtitle.
- Added hover-mode CSS so the primary bar stays invisible until its location is hovered, then becomes visible and interactable.
Tests:
- `bun test src/config/resolve/subtitle-style.test.ts src/config/config.test.ts src/renderer/handlers/keyboard.test.ts src/renderer/subtitle-render.test.ts src/main/runtime/config-hot-reload-handlers.test.ts`
- `bun run typecheck`
- `bun run test:config`
- `bun run test:fast`
- `bun run test:env`
- `bun run build`
- `bun run test:smoke:dist`
- `bun run docs:test`
- `bun run docs:build`
- `bun run format:check:src`
- `bun run changelog:lint`
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,5 @@
type: added
area: dictionary
- Added CLI and in-app AniList selection for character dictionary mismatches, with series-scoped overrides that replace stale wrong-title entries in the merged dictionary.
- Added launcher support through `subminer dictionary --candidates` and `subminer dictionary --select`, plus a default `Ctrl+Alt+A` shortcut for the in-app selector.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed Linux multi-line subtitle copy timing out after the prompt by letting the overlay handle the follow-up digit locally.
@@ -0,0 +1,4 @@
type: fixed
area: tokenizer
- Stopped standalone `あ` interjections from receiving subtitle annotation metadata such as N+1, JLPT, and frequency highlighting when POS tags are unavailable.
@@ -0,0 +1,4 @@
type: added
area: overlay
- Added a `V` shortcut and mpv plugin binding to toggle the SubMiner primary subtitle bar without changing mpv native subtitle visibility.
@@ -0,0 +1,4 @@
type: fixed
area: mpv
- Stopped mpv from owning long-running SubMiner AppImage subprocesses during playback shutdown, preventing desktop crash notifications when closing video.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed annotated subtitle token colors so `subtitleStyle` typography is preserved and higher-priority known-word/frequency colors are not overridden by JLPT colors.
@@ -0,0 +1,4 @@
type: fixed
area: tokenizer
- Stopped kana-only grammar-helper merges such as `ことに` from receiving subtitle annotation metadata like N+1, JLPT, known-word, or frequency highlighting.
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Anki: Manual clipboard subtitle updates now replace both expression and sentence audio fields even when configured audio overwrite is disabled.
@@ -0,0 +1,4 @@
type: fixed
area: config
- Accepted `subtitleStyle.hoverBackground` as an alias for `subtitleStyle.hoverTokenBackgroundColor`, so setting it to `transparent` removes hover token backgrounds.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: launcher
- Launcher-managed playback now exits the background SubMiner app when the video closes, while explicit background launches stay persistent.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Added a subtle brightness lift for annotated subtitle token hover states so transparent hover backgrounds still show a visible hover affordance.
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: tray
- Tray: Replaced the Open Overlay tray menu item with Open Help, which opens the session help modal.
@@ -0,0 +1,6 @@
type: fixed
area: tokenizer
- Use Yomitan `wordClasses` metadata for subtitle POS filtering.
- Backfill blank MeCab POS detail fields during parser enrichment.
- Keep subtitle annotation metadata stripped from token results.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Fixed Hyprland fullscreen transitions so mpv fullscreen changes refresh visible overlay geometry, reassert topmost stacking, and keep primary subtitle hover pause working after resize/toggle cycles.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: stats
- Stats background mode now routes through the isolated stats daemon instead of starting the regular SubMiner app runtime.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: tokenizer
- Stopped kana-only subtitle tokens from being selected as N+1 targets.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: stats
- In-app stats startup now defers to an already-running background stats daemon instead of starting a second stats server.
+5
View File
@@ -0,0 +1,5 @@
type: internal
area: release
- Replaced the changelog renderer with a `claude -p` polish pass that merges related fragments, drops PR housekeeping, and writes user-friendly release notes. CHANGELOG.md keeps internal items in a collapsed `<details>` block; the GitHub release notes drop them entirely.
- Removed the release CI auto-build for pending `changes/*.md` fragments. Tag-based release runs now fail fast with a clear error if fragments are still pending; build the changelog locally with `bun run changelog:build` (which requires the `claude` CLI on PATH) and commit before tagging.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Overlay: Restored persistent JLPT subtitle underlines while keeping hover JLPT labels and annotation color priority intact.
@@ -0,0 +1,4 @@
type: fixed
area: shortcuts
- Accept follow-up number-row digits for multi-line subtitle mining even when the original shortcut modifiers are still held.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Suppressed subtitle annotation styling for standalone auxiliary inflection fragments such as `れる` and `れた` while keeping lexical `くれる` forms eligible for lookup metadata.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Suppressed subtitle annotation styling for grammar-only endings such as `じゃないですか` and standalone polite copula tails like `です` / `ですよ`.
@@ -0,0 +1,4 @@
type: fixed
area: mpv
- Kept the visible overlay alive across same-media mpv reloads during buffering, avoiding duplicate startup gates and AniSkip lookups.
+5
View File
@@ -0,0 +1,5 @@
type: fixed
area: jellyfin
- Improved Jellyfin setup with recent server selection and inline authentication feedback.
- Added a tray Jellyfin Discovery toggle for runtime-only cast discovery.
@@ -0,0 +1,4 @@
type: fixed
area: stats
- Kept regular app stats routing isolated from a separately running background stats daemon during playback startup.
+4
View File
@@ -0,0 +1,4 @@
type: added
area: texthooker
- Texthooker: Added `subminer texthooker -o` and a tray menu item to open the local texthooker page in the default browser.
+4
View File
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Overlay: Kept JLPT subtitle underlines at their JLPT color after dictionary lookups, even when the token also has another annotation color.
@@ -0,0 +1,4 @@
type: fixed
area: tokenizer
- Tokenizer: Suppress annotations for ハァ-style interjection subtitles.
@@ -0,0 +1,4 @@
type: fixed
area: overlay
- Overlay: Refresh the current subtitle after successful card mining so newly known words recolor immediately.
@@ -0,0 +1,5 @@
type: fixed
area: tokenizer
- Tokenizer: Replaced hard-coded standalone grammar-ending permutations with shared pattern matching for polite copula, negative copula, and explanatory subtitle endings.
- Tokenizer: Kept grammar annotation exclusion logic in the shared subtitle filter and removed stale duplicate exclusion helpers from the annotation stage.
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Anki: Manual clipboard subtitle updates now preserve existing word audio while replacing sentence audio and animated-image media.

Some files were not shown because too many files have changed in this diff Show More