Compare commits

..

34 Commits

Author SHA1 Message Date
sudacode 0f849da777 harden bootstrap version load and clean plugin on uninstall
- Use pcall for version.lua in bootstrap.lua so missing version module does not crash plugin startup
- Remove plugin/subminer from app-data dirs in uninstall-linux and uninstall-macos targets
- Add Lua compat test asserting bootstrap uses defensive pcall for version load
- Add release-workflow test asserting uninstall targets clean bundled plugin dirs
- Delete completed planning document
2026-05-12 20:03:02 -07:00
sudacode 75348aa72a feat: inject bundled mpv plugin for managed launches, remove legacy glob
- SubMiner-managed launcher and Windows shortcut launches inject the bundled plugin when no global plugin is detected
- First-run setup detects and removes legacy global plugin files via OS trash before managed playback starts
- Makefile `install-plugin` target and Windows config-rewrite script removed; Linux/macOS install now copies plugin to app data dir
- AniList stats search and post-watch tracking now go through the shared rate limiter
- Stats cover-art lookup reuses cached AniList data before issuing a new request
- Closing mpv in a launcher-managed session now terminates the background Electron app
2026-05-12 19:40:26 -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
sudacode 7ac51cd5e9 chore(release): prepare v0.12.0 2026-04-11 21:54:00 -07:00
sudacode 52bab1d611 Windows update (#49) 2026-04-11 21:45:52 -07:00
sudacode 49e46e6b9b chore(repo): update vendor and backlog tasks 2026-04-11 14:53:06 -07:00
sudacode c1c40c8d40 fix(immersion-tracker): preserve timestamps under Bun libsql 2026-04-11 14:49:54 -07:00
sudacode c71482cb44 fix(mpv-plugin): restore Lua parser compatibility 2026-04-11 14:49:46 -07:00
sudacode 05cf4a6fe5 feat(stats): dashboard updates (#50) 2026-04-10 02:46:50 -07:00
sudacode 9b4de93283 chore(release): prepare v0.11.2 2026-04-07 01:23:18 -07:00
sudacode 16ffbbc4b3 docs: update wayland support note 2026-04-07 01:14:57 -07:00
sudacode de4f3efa30 docs: add mpv.launchMode to config docs, add changelog:docs generator, format
- Document the new mpv.launchMode option in the configuration docs page
- Add changelog:docs command to auto-generate docs-site/changelog.md from root CHANGELOG.md
- Add breaking changes support to the changelog fragment generator
- Fix docs-sync test to only compare current minor release headings
- Apply prettier formatting to source files
2026-04-07 01:06:43 -07:00
Autumn (Bee) bc7dde3b02 [codex] Replace mpv fullscreen toggle with launch mode config (#48)
Co-authored-by: bee <autumn@skerritt.blog>
2026-04-07 00:38:15 -07:00
sudacode 7a64488ed5 docs: refresh README and docs site guidance 2026-04-06 01:19:15 -07:00
Autumn (Bee) 5f3c3871d3 [codex] Prefer unlabeled external sidecars for local playback (#46)
Co-authored-by: bee <autumn@skerritt.blog>
2026-04-05 22:07:56 -07:00
sudacode 4d24e22bb5 fix: force X11 mpv fallback for launcher-managed playback (#47) 2026-04-05 15:32:45 -07:00
sudacode c47cfb52af [codex] Fix Linux AppImage libffmpeg child-process startup (#45) 2026-04-05 15:22:57 -07:00
sudacode da0087bba6 add --setup flag to force re-open first-run setup wizard
- `openFirstRunSetupWindow` accepts a `force` parameter that bypasses the completed-state guard
- `--setup` arg sets `force=true` so the wizard opens even after setup is done
- README and docs updated to document `subminer app --setup` as the explicit setup command
- Fix docs tip: `subminer --setup` → `subminer app --setup`
- Collapse extra launch examples into a `<details>` block in installation.md
2026-04-05 01:29:55 -07:00
sudacode 8338f27794 docs: reorder setup steps to put first launch before verify
- Move "First Launch" before "Verify Setup" in README quick-start steps
- Consolidate docs-site installation guide: merge "Verify Installation" into "Verify Setup" section after first-run setup
2026-04-04 21:45:32 -07:00
sudacode b029d65c90 docs: add playlist-browser screenshot to README
- Add playlist-browser.png screenshot and embed it in README
- Update stats-overview.png
- Minor formatting fixes (table alignment, double space)
2026-04-04 21:35:46 -07:00
sudacode c24f99899b docs: add v0.11.1 changelog and expand installation guide
- Add v0.11.1 release entry with Wayland app-id and shortcut regression fixes
- Add "How the Pieces Fit Together" overview section to installation
- Add per-distro dependency install snippets (Arch, Ubuntu/Debian, Fedora, macOS, Windows)
- Add Windows prerequisites and getting-started steps
- Add First-Run Setup and Anki Setup sections; move Rofi theme to Optional Extras
- Expand Bun launcher requirement into explicit install step
- Reformat tables and callouts in usage.md for consistency
2026-04-04 15:28:13 -07:00
sudacode 3aca581764 docs: replace GitHub callout blocks with plain text in README 2026-04-04 14:41:38 -07:00
sudacode ba540d09b2 docs: expand launcher and Windows guidance across README and docs-site
- Add downloads/release/license/TypeScript badges to README, replace static badge set
- Add macOS launcher install instructions (wget/curl + make install-macos)
- Add Windows experimental warning and bun run subminer clarification
- Update launcher-script.md: rename "wrapper" to "launcher", add Windows tip
- Update usage.md: add Windows mpv shortcut row, clarify shebang behavior per platform
- Update installation.md: mkdir -p, improve Linux/macOS launcher sections
2026-04-04 14:37:58 -07:00
sudacode 6530d2ccbc Clarify install requirements and dependency checks
- Reclassify ffmpeg and MeCab as recommended or optional
- Add `subminer doctor` install verification step
- Refresh platform-specific install guidance and first-run tips
2026-04-04 12:51:37 -07:00
562 changed files with 37141 additions and 4365 deletions
+406
View File
@@ -0,0 +1,406 @@
name: Prerelease
on:
push:
tags:
- 'v*-beta.*'
- 'v*-rc.*'
concurrency:
group: prerelease-${{ github.ref }}
cancel-in-progress: false
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Lint stats (formatting)
run: bun run lint:stats
- name: Build (TypeScript check)
run: bun run typecheck
- name: Test suite (source)
run: bun run test:fast
- name: Environment suite
run: bun run test:env
- name: Coverage suite (maintained source lane)
run: bun run test:coverage:src
- name: Upload coverage artifact
uses: actions/upload-artifact@v4
with:
name: coverage-test-src
path: coverage/test-src/lcov.info
if-no-files-found: error
- name: Launcher smoke suite (source)
run: bun run test:launcher:smoke:src
- name: Upload launcher smoke artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: launcher-smoke
path: .tmp/launcher-smoke/**
if-no-files-found: ignore
- name: Build (bundle)
run: bun run build
- name: Immersion SQLite verification
run: bun run test:immersion:sqlite:dist
- name: Dist smoke suite
run: bun run test:smoke:dist
build-linux:
needs: [quality-gate]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install
bun run build
- name: Build AppImage
run: bun run build:appimage
- name: Build unversioned AppImage
run: |
shopt -s nullglob
appimages=(release/SubMiner-*.AppImage)
if [ "${#appimages[@]}" -eq 0 ]; then
echo "No versioned AppImage found to create unversioned artifact."
ls -la release
exit 1
fi
cp "${appimages[0]}" release/SubMiner.AppImage
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: appimage
path: release/*.AppImage
if-no-files-found: error
build-macos:
needs: [quality-gate]
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Validate macOS signing/notarization secrets
run: |
missing=0
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
if [ -z "${!name}" ]; then
echo "Missing required secret: $name"
missing=1
fi
done
if [ "$missing" -ne 0 ]; then
echo "Set all required macOS signing/notarization secrets and rerun."
exit 1
fi
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
run: |
cd vendor/texthooker-ui
bun install
bun run build
- name: Build signed + notarized macOS artifacts
run: bun run build:mac
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos
path: |
release/*.dmg
release/*.zip
if-no-files-found: error
build-windows:
needs: [quality-gate]
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
stats/node_modules
vendor/texthooker-ui/node_modules
vendor/subminer-yomitan/node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock', 'stats/bun.lock', 'vendor/texthooker-ui/package.json', 'vendor/subminer-yomitan/package-lock.json') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: |
bun install --frozen-lockfile
cd stats && bun install --frozen-lockfile
- name: Build texthooker-ui
shell: powershell
run: |
Set-Location vendor/texthooker-ui
bun install
bun run build
- name: Build unsigned Windows artifacts
run: bun run build:win:unsigned
- name: Upload Windows artifacts
uses: actions/upload-artifact@v4
with:
name: windows
path: |
release/*.exe
release/*.zip
if-no-files-found: error
release:
needs: [build-linux, build-macos, build-windows]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download AppImage
uses: actions/download-artifact@v4
with:
name: appimage
path: release
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
name: macos
path: release
- name: Download Windows artifacts
uses: actions/download-artifact@v4
with:
name: windows
path: release
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.5
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build Bun subminer wrapper
run: make build-launcher
- name: Verify Bun subminer wrapper
run: dist/launcher/subminer --help >/dev/null
- name: Enforce generated launcher workflow
run: bash scripts/verify-generated-launcher.sh
- name: Verify generated config examples
run: bun run verify:config-example
- name: Package optional assets bundle
run: |
tar -czf "release/subminer-assets.tar.gz" \
config.example.jsonc \
plugin/subminer \
plugin/subminer.conf \
assets/themes/subminer.rasi
- name: Generate checksums
run: |
shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz dist/launcher/subminer)
if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation."
exit 1
fi
: > release/SHA256SUMS.txt
for file in "${files[@]}"; do
printf '%s %s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"${file##*/}" >> release/SHA256SUMS.txt
done
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Generate prerelease notes from pending fragments
run: bun run changelog:prerelease-notes --version "${{ steps.version.outputs.VERSION }}"
- name: Publish Prerelease
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
shopt -s nullglob
artifacts=(
release/*.AppImage
release/*.dmg
release/*.exe
release/*.zip
release/*.tar.gz
release/SHA256SUMS.txt
dist/launcher/subminer
)
if [ "${#artifacts[@]}" -eq 0 ]; then
echo "No release artifacts found for upload."
exit 1
fi
if gh release view "${{ steps.version.outputs.VERSION }}" >/dev/null 2>&1; then
gh release edit "${{ steps.version.outputs.VERSION }}" \
--draft \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
else
gh release create "${{ steps.version.outputs.VERSION }}" \
--draft \
--latest=false \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
fi
for asset in "${artifacts[@]}"; do
gh release upload "${{ steps.version.outputs.VERSION }}" "$asset" --clobber
done
gh release edit "${{ steps.version.outputs.VERSION }}" \
--draft=false \
--prerelease \
--title "${{ steps.version.outputs.VERSION }}" \
--notes-file release/prerelease-notes.md
+11 -5
View File
@@ -4,6 +4,8 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
- '!v*-beta.*'
- '!v*-rc.*'
concurrency: concurrency:
group: release-${{ github.ref }} group: release-${{ github.ref }}
@@ -338,18 +340,22 @@ jobs:
echo "No release artifacts found for checksum generation." echo "No release artifacts found for checksum generation."
exit 1 exit 1
fi fi
sha256sum "${files[@]}" > release/SHA256SUMS.txt : > release/SHA256SUMS.txt
for file in "${files[@]}"; do
printf '%s %s\n' \
"$(sha256sum "$file" | awk '{print $1}')" \
"${file##*/}" >> release/SHA256SUMS.txt
done
- name: Get version from tag - name: Get version from tag
id: version id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
- name: Build changelog artifacts for release - name: Guard against pending changelog fragments
run: | run: |
if find changes -maxdepth 1 -name '*.md' -not -name README.md -print -quit | grep -q .; then 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 }}" 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."
else exit 1
echo "No pending changelog fragments found."
fi fi
- name: Verify changelog is ready for tagged release - name: Verify changelog is ready for tagged release
+48
View File
@@ -1,5 +1,53 @@
# Changelog # Changelog
## v0.12.0 (2026-04-11)
### Changed
- Overlay: Added configurable overlay shortcuts for session help, controller select, and controller debug actions.
- Overlay: Added mpv/plugin and CLI routing for session help, controller utilities, and subtitle sidebar toggling through the shared session-action path.
- Overlay: Improved dedicated overlay modal retry and focus handling for runtime options, Jimaku, session help, controller tools, and the playlist browser.
- Overlay: Fixed controller configuration and controller debug shortcut opens so configured bindings bring up their modals again instead of tripping renderer recovery.
- Stats: Sessions are rolled up per episode within each day, with a bulk delete that wipes every session in the group.
- Stats: Trends add a 365-day range next to the existing 7d/30d/90d/all options.
- Stats: Library detail view gets a delete-episode action that removes the video and all its sessions.
- Stats: Vocabulary Top 50 tightens the word/reading column so katakana entries no longer push the scores off screen.
- Stats: Episode detail hides card events whose Anki notes have been deleted, instead of showing phantom mining activity.
- Stats: Trend and watch-time charts share a unified theme with horizontal gridlines and larger ticks for legibility.
- Stats: Overview, Library, Trends, Sessions, and Vocabulary now use generic "title" wording so YouTube videos and anime live comfortably side by side in the dashboard.
- Stats: Session timeline no longer plots seek-forward/seek-backward markers — they were too noisy on sessions with lots of rewinds.
- Stats: Replaced the "Library — Per Day" section on the Stats → Trends page with a "Library — Summary" section. The new section shows a top-10 watch-time leaderboard chart and a sortable per-title table (watch time, videos, sessions, cards, words, lookups, lookups/100w, date range), all scoped to the current date range selector.
### Fixed
- Overlay: Fixed overlay drag-and-drop routing so dropping external subtitle files like `.ass` onto mpv still loads them when the overlay is visible.
- Overlay: Addressed the latest CodeRabbit follow-ups on PR #49, including generation-scoped Lua session binding names, stricter session command validation, session-help shortcut visibility, the numeric-selection key guard, stats-overlay startup classification, and safer session-binding persistence.
- Overlay: Addressed the latest CodeRabbit follow-ups on the Windows overlay flow, including exact mpv target resolution, lower-overlay helper arguments, Win32 failure detection, and overlay cleanup on tracker loss.
- Overlay: Fixed Windows overlay z-order so the visible subtitle overlay stops staying above unrelated apps after mpv loses focus.
- Overlay: Fixed Windows overlay tracking to use native window polling and owner/z-order binding, which keeps the subtitle overlay aligned to the active mpv window more reliably.
- Overlay: Fixed Windows overlay hide/restore behavior so minimizing mpv immediately hides the overlay and restoring mpv brings it back on top of the mpv window without requiring a click.
- Overlay: Fixed stats overlay layering so the in-player stats page now stays above mpv and the subtitle overlay while it is open.
- Overlay: Fixed Windows subtitle overlay stability so transient tracker misses and restore events keep the current subtitle visible instead of waiting for the next subtitle line.
- Overlay: Fixed Windows focus handoff from the interactive subtitle overlay back to mpv so the overlay no longer drops behind mpv and briefly disappears.
- Overlay: Fixed Windows visible-overlay startup so it no longer briefly opens as an interactive or opaque surface before the tracked transparent overlay state settles.
- Overlay: Fixed spurious auto-pause after overlay visibility recovery and window resize so the overlay no longer pauses mpv until the pointer genuinely re-enters the subtitle area.
- Overlay: Fixed Windows secondary subtitle hover mode so the expanded hover hit area no longer blocks the native minimize, maximize, and close buttons.
- Overlay: Fixed Windows Yomitan popup focus loss after closing nested lookups so the original popup stays interactive instead of falling through to mpv.
- Stats: Fixed immersion-tracker timestamp handling under Bun/libsql so library rows, session timelines, and lifetime summaries keep real wall-clock millisecond values instead of truncating to invalid negative timestamps.
- Mpv Plugin: Fixed the mpv Lua plugin so hover and environment modules no longer use the `goto continue` pattern that can fail to parse on some user Lua runtimes.
### Internal
- Release: Added a dedicated beta/rc prerelease GitHub Actions workflow that publishes GitHub prereleases without consuming pending changelog fragments or updating AUR.
- Release: Added prerelease note generation so beta and release-candidate tags can reuse the current pending `changes/*.md` fragments while leaving stable changelog publication for the final release cut.
## v0.11.2 (2026-04-07)
### Changed
- Launcher: Replaced the launcher-only fullscreen toggle with `mpv.launchMode` so SubMiner-managed mpv playback can start in normal, maximized, or fullscreen mode.
### Fixed
- Launcher: Fixed launcher-managed mpv spawning to force an explicit X11 GPU path when Wayland trackers are unavailable.
- Launcher: Local playback now promotes a single unlabeled external subtitle sidecar to the primary slot instead of leaving mpv's embedded English auto-selection in place.
- Release: Fixed Linux AppImage startup packaging so Chromium child relaunches can resolve the bundled `libffmpeg.so` instead of crash-looping on startup.
## v0.11.1 (2026-04-04) ## v0.11.1 (2026-04-04)
### Fixed ### Fixed
+12 -21
View File
@@ -1,10 +1,9 @@
.PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows install-plugin uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop .PHONY: help deps build build-launcher install build-linux build-macos build-macos-unsigned clean install-linux install-macos install-windows uninstall uninstall-linux uninstall-macos uninstall-windows print-dirs pretty lint ensure-bun generate-config generate-example-config dev-start dev-start-macos dev-watch dev-watch-macos dev-toggle dev-stop
APP_NAME := subminer APP_NAME := subminer
THEME_SOURCE := assets/themes/subminer.rasi THEME_SOURCE := assets/themes/subminer.rasi
LAUNCHER_OUT := dist/launcher/$(APP_NAME) LAUNCHER_OUT := dist/launcher/$(APP_NAME)
THEME_FILE := subminer.rasi THEME_FILE := subminer.rasi
PLUGIN_CONF := plugin/subminer.conf
# Default install prefix for the wrapper script. # Default install prefix for the wrapper script.
PREFIX ?= $(HOME)/.local PREFIX ?= $(HOME)/.local
@@ -64,8 +63,7 @@ help:
" dev-stop Stop a running local Electron app" \ " dev-stop Stop a running local Electron app" \
" install-linux Install Linux wrapper/theme/app artifacts" \ " install-linux Install Linux wrapper/theme/app artifacts" \
" install-macos Install macOS wrapper/theme/app artifacts" \ " install-macos Install macOS wrapper/theme/app artifacts" \
" install-windows Install Windows mpv plugin artifacts" \ " install-windows Print Windows packaging/install guidance" \
" install-plugin Install mpv Lua plugin and plugin config" \
" generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \ " generate-config Generate ~/.config/SubMiner/config.jsonc from centralized defaults" \
"" \ "" \
"Other targets:" \ "Other targets:" \
@@ -200,6 +198,8 @@ install-linux: build-launcher
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)" @install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
@install -d "$(LINUX_DATA_DIR)/themes" @install -d "$(LINUX_DATA_DIR)/themes"
@install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)" @install -m 0644 "./$(THEME_SOURCE)" "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
@install -d "$(LINUX_DATA_DIR)/plugin/subminer"
@cp -R ./plugin/subminer/. "$(LINUX_DATA_DIR)/plugin/subminer/"
@if [ -n "$(APPIMAGE_SRC)" ]; then \ @if [ -n "$(APPIMAGE_SRC)" ]; then \
install -m 0755 "$(APPIMAGE_SRC)" "$(BINDIR)/SubMiner.AppImage"; \ install -m 0755 "$(APPIMAGE_SRC)" "$(BINDIR)/SubMiner.AppImage"; \
else \ else \
@@ -214,6 +214,8 @@ install-macos: build-launcher
@install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)" @install -m 0755 "$(LAUNCHER_OUT)" "$(BINDIR)/$(APP_NAME)"
@install -d "$(MACOS_DATA_DIR)/themes" @install -d "$(MACOS_DATA_DIR)/themes"
@install -m 0644 "./$(THEME_SOURCE)" "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)" @install -m 0644 "./$(THEME_SOURCE)" "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
@install -d "$(MACOS_DATA_DIR)/plugin/subminer"
@cp -R ./plugin/subminer/. "$(MACOS_DATA_DIR)/plugin/subminer/"
@install -d "$(MACOS_APP_DIR)" @install -d "$(MACOS_APP_DIR)"
@if [ -n "$(MACOS_APP_SRC)" ]; then \ @if [ -n "$(MACOS_APP_SRC)" ]; then \
rm -rf "$(MACOS_APP_DEST)"; \ rm -rf "$(MACOS_APP_DEST)"; \
@@ -230,21 +232,8 @@ install-macos: build-launcher
@printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)" @printf '%s\n' "Installed to:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)"
install-windows: install-windows:
@printf '%s\n' "[INFO] Installing Windows mpv plugin artifacts" @printf '%s\n' "[INFO] Windows builds run via: bun run build:win"
@$(MAKE) --no-print-directory install-plugin @printf '%s\n' "[INFO] SubMiner-managed mpv launches inject the bundled runtime plugin; no global mpv plugin install is needed."
install-plugin:
@printf '%s\n' "[INFO] Installing mpv plugin artifacts"
@install -d "$(MPV_SCRIPTS_DIR)"
@rm -f "$(MPV_SCRIPTS_DIR)/subminer.lua" "$(MPV_SCRIPTS_DIR)/subminer-loader.lua"
@install -d "$(MPV_SCRIPTS_DIR)/subminer"
@install -d "$(MPV_SCRIPT_OPTS_DIR)"
@cp -R ./plugin/subminer/. "$(MPV_SCRIPTS_DIR)/subminer/"
@install -m 0644 "./$(PLUGIN_CONF)" "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
@if [ "$(PLATFORM)" = "windows" ]; then \
bun ./scripts/configure-plugin-binary-path.mjs "$(MPV_SCRIPT_OPTS_DIR)/subminer.conf" "$(CURDIR)" win32; \
fi
@printf '%s\n' "Installed to:" " $(MPV_SCRIPTS_DIR)/subminer/main.lua" " $(MPV_SCRIPTS_DIR)/subminer/" " $(MPV_SCRIPT_OPTS_DIR)/subminer.conf"
uninstall: uninstall:
@printf '%s\n' "[INFO] Detected platform: $(PLATFORM)" @printf '%s\n' "[INFO] Detected platform: $(PLATFORM)"
@@ -258,13 +247,15 @@ uninstall:
uninstall-linux: uninstall-linux:
@rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage" @rm -f "$(BINDIR)/subminer" "$(BINDIR)/SubMiner.AppImage"
@rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)" @rm -f "$(LINUX_DATA_DIR)/themes/$(THEME_FILE)"
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" @rm -rf "$(LINUX_DATA_DIR)/plugin/subminer"
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(BINDIR)/SubMiner.AppImage" " $(LINUX_DATA_DIR)/themes/$(THEME_FILE)" " $(LINUX_DATA_DIR)/plugin/subminer"
uninstall-macos: uninstall-macos:
@rm -f "$(BINDIR)/subminer" @rm -f "$(BINDIR)/subminer"
@rm -f "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)" @rm -f "$(MACOS_DATA_DIR)/themes/$(THEME_FILE)"
@rm -rf "$(MACOS_DATA_DIR)/plugin/subminer"
@rm -rf "$(MACOS_APP_DEST)" @rm -rf "$(MACOS_APP_DEST)"
@printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_APP_DEST)" @printf '%s\n' "Removed:" " $(BINDIR)/subminer" " $(MACOS_DATA_DIR)/themes/$(THEME_FILE)" " $(MACOS_DATA_DIR)/plugin/subminer" " $(MACOS_APP_DEST)"
uninstall-windows: uninstall-windows:
@rm -rf "$(MPV_SCRIPTS_DIR)/subminer" @rm -rf "$(MPV_SCRIPTS_DIR)/subminer"
+68 -30
View File
@@ -4,25 +4,21 @@
# SubMiner # SubMiner
## Turn mpv into a sentence-mining workstation.
Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv. Look up words with Yomitan, export to Anki in one key, track your immersion — all without leaving mpv.
[![License: GPL v3](https://img.shields.io/badge/license-GPLv3-1a1a2e?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0) [Installation](#quick-start) · [Requirements](#requirements) · [Usage](https://docs.subminer.moe/usage) · [Documentation](https://docs.subminer.moe)
[![Platform](https://img.shields.io/badge/platform-Linux%20·%20macOS%20·%20Windows-1a1a2e?style=flat-square)](https://github.com/ksyasuda/SubMiner)
[![Docs](https://img.shields.io/badge/docs-docs.subminer.moe-e6a817?style=flat-square)](https://docs.subminer.moe) [![Downloads](https://img.shields.io/github/downloads/ksyasuda/SubMiner/total?style=flat-square&color=1a1a2e)](https://github.com/ksyasuda/SubMiner/releases)
[![Release](https://img.shields.io/github/v/release/ksyasuda/SubMiner?style=flat-square&color=1a1a2e)](https://github.com/ksyasuda/SubMiner/releases/latest)
[![AUR](https://img.shields.io/aur/version/subminer-bin?style=flat-square&color=1a1a2e)](https://aur.archlinux.org/packages/subminer-bin) [![AUR](https://img.shields.io/aur/version/subminer-bin?style=flat-square&color=1a1a2e)](https://aur.archlinux.org/packages/subminer-bin)
[![Platform](https://img.shields.io/badge/platform-Linux%20·%20macOS%20·%20Windows-1a1a2e?style=flat-square)](https://github.com/ksyasuda/SubMiner)
[![License](https://img.shields.io/github/license/ksyasuda/SubMiner?style=flat-square&color=1a1a2e)](https://www.gnu.org/licenses/gpl-3.0)
[![TypeScript](https://img.shields.io/badge/TypeScript-1a1a2e?style=flat-square&logo=typescript&logoColor=3178c6)](https://www.typescriptlang.org)
[![SubMiner demo](./assets/minecard.webp)](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2) [![SubMiner demo](./assets/minecard.webp)](https://github.com/user-attachments/assets/89e61895-e2b7-4b47-8d50-a35afe4132b2)
</div> </div>
## How It Works
SubMiner runs as an invisible Electron overlay on top of mpv. Subtitles render as an interactive layer. Move your cursor over any word and trigger a [Yomitan](https://github.com/yomidevs/yomitan) lookup. Press one key to snapshot the sentence, audio, and screenshot into Anki via AnkiConnect.
First-run setup requires the mpv plugin before it can finish. On Windows, the optional `SubMiner mpv` shortcut created during setup is the recommended playback entry point because it launches `mpv` with SubMiner's defaults directly, so you do not need an `mpv.conf` profile just to use it.
## Features ## Features
### Dictionary Lookups ### Dictionary Lookups
@@ -69,7 +65,9 @@ Local stats dashboard — watch time, anime library, vocabulary growth, mining t
Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback. Browse sibling episode files and the active mpv queue in one overlay modal. Open it with `Ctrl+Alt+P` to append episodes from the current directory, jump to queued items, remove entries, or reorder the playlist without leaving playback.
Managed local playback now reapplies your configured subtitle language priorities after mpv loads track metadata, so mixed subtitle sets can settle onto the expected primary and secondary tracks instead of staying on mpv's initial `sid=auto` guess. <div align="center">
<img src="docs-site/public/screenshots/playlist-browser.png" width="800" alt="Stats dashboard showing watch time, cards mined, streaks, and tracking data">
</div>
<br> <br>
@@ -86,7 +84,7 @@ Managed local playback now reapplies your configured subtitle language prioritie
</tr> </tr>
<tr> <tr>
<td><b>Jellyfin</b></td> <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>
<tr> <tr>
<td><b>Jimaku</b></td> <td><b>Jimaku</b></td>
@@ -112,32 +110,38 @@ Managed local playback now reapplies your configured subtitle language prioritie
## Requirements ## Requirements
| | Required | Optional | | | Required | Recommended | Optional |
| -------------- | --------------------------------------- | ---------------------------------------------------------- | | -------------- | --------------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| **Player** | [`mpv`](https://mpv.io) with IPC socket | — | | **Player** | [`mpv`](https://mpv.io) with IPC socket | — | — |
| **Processing** | `ffmpeg`, `mecab` + `mecab-ipadic` | `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) | | **Processing** | — | `ffmpeg` (audio clips & screenshots) | `mecab` + `mecab-ipadic` (annotation POS filtering), `guessit` (AniSkip), `alass` / `ffsubsync` (subtitle sync) |
| **Media** | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` | | **Media** | — | — | `yt-dlp`, `chafa`, `ffmpegthumbnailer` |
| **Selection** | — | `fzf` / `rofi` | | **Selection** | — | — | `fzf` / `rofi` |
> [!TIP]
> `ffmpeg` is not strictly required to run SubMiner, but without it audio clips and screenshots will not be attached to Anki cards. Most users will want it installed.
> [!NOTE] > [!NOTE]
> [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it. > [`bun`](https://bun.sh) is required if building from source or using the CLI wrapper: `subminer`. Pre-built releases (AppImage, DMG, installer) do not require it.
**Platform-specific:** **Platform-specific:**
| Linux | macOS | Windows | | Linux | macOS | Windows |
| ----------------------------------- | ------------------------ | ------------- | | ------------------------------------------------------------ | ------------------------ | ------------- |
| `hyprctl` or `xdotool` + `xwininfo` | Accessibility permission | No extra deps | | Hyprland (`hyprctl`) · X11/Xwayland (`xdotool` + `xwininfo`) | Accessibility permission | No extra deps |
> [!NOTE]
> **Wayland support is compositor-specific.** Wayland has no universal API for window positioning and each compositor exposes its own IPC, so SubMiner needs a dedicated backend per compositor. Hyprland is the only native Wayland backend supported currenlty. All other Linux compositors require both mpv and SubMiner to run under X11 or Xwayland. The launcher detects your compositor and configures this automatically.
<details> <details>
<summary><b>Arch Linux</b></summary> <summary><b>Arch Linux</b></summary>
```bash ```bash
paru -S --needed mpv ffmpeg mecab-git mecab-ipadic paru -S --needed mpv ffmpeg
# Optional # Optional
paru -S --needed yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo paru -S --needed mecab-git mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer xdotool xorg-xwininfo
# Optional: subtitle sync (install at least one for subtitle syncing to work) # Optional: subtitle sync (install at least one for subtitle syncing to work)
paru -S --needed alass python-ffsubsync paru -S --needed alass python-ffsubsync
# X11 / XWAYLAND # X11 / Xwayland (required for non-Hyprland compositors)
paru -S --needed xdotool xorg-xwininfo paru -S --needed xdotool xorg-xwininfo
``` ```
@@ -147,9 +151,9 @@ paru -S --needed xdotool xorg-xwininfo
<summary><b>macOS</b></summary> <summary><b>macOS</b></summary>
```bash ```bash
brew install mpv ffmpeg mecab mecab-ipadic brew install mpv ffmpeg
# Optional # Optional
brew install yt-dlp fzf rofi chafa ffmpegthumbnailer brew install mecab mecab-ipadic yt-dlp fzf rofi chafa ffmpegthumbnailer
# Optional: subtitle sync (install at least one for subtitle syncing to work) # Optional: subtitle sync (install at least one for subtitle syncing to work)
brew install alass brew install alass
pip install ffsubsync pip install ffsubsync
@@ -164,7 +168,7 @@ Grant Accessibility permission to SubMiner in **System Settings > Privacy & Secu
Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`. Install [`mpv`](https://mpv.io/installation/) and [`ffmpeg`](https://ffmpeg.org/download.html) and ensure both are on your `PATH`.
For MeCab, install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary. Optionally install [MeCab for Windows](https://taku910.github.io/mecab/#download) with the UTF-8 dictionary for additional metadata enrichment.
</details> </details>
@@ -210,6 +214,16 @@ wget https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -O ~
Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`. Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest) and drag `SubMiner.app` into `/Applications`.
Also download the `subminer` launcher (recommended):
```bash
sudo curl -fSL https://github.com/ksyasuda/SubMiner/releases/latest/download/subminer -o /usr/local/bin/subminer \
&& sudo chmod +x /usr/local/bin/subminer
```
> [!NOTE]
> The `subminer` launcher uses a [Bun](https://bun.sh) shebang. Make sure `bun` is on your `PATH`.
</details> </details>
<details> <details>
@@ -217,6 +231,10 @@ Download the latest DMG or ZIP from [GitHub Releases](https://github.com/ksyasud
Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`. Download the latest installer or portable `.zip` from [GitHub Releases](https://github.com/ksyasuda/SubMiner/releases/latest). Make sure `mpv` is on your `PATH`.
**Windows support is experimental.** Core features such as mining, annotations, and dictionary lookups work, but some functionality may be missing or unstable. Bug reports welcome.
**Note:** On Windows the `subminer` launcher requires [`bun`](https://bun.sh) and must be invoked with `bun run subminer` instead of running the script directly. The recommended alternative is the **SubMiner mpv** shortcut created during first-run setup — double-click it, drag files onto it, or run `SubMiner.exe --launch-mpv` from a terminal. See the [Windows mpv Shortcut](https://docs.subminer.moe/usage#windows-mpv-shortcut) section for details.
</details> </details>
<details> <details>
@@ -228,9 +246,27 @@ See the [build-from-source guide](https://docs.subminer.moe/installation#from-so
### 2. First Launch ### 2. First Launch
Run the app. On first launch SubMiner starts in the system tray, creates a default config, and opens a setup popup to finish config, install the mpv plugin, and configure Yomitan dictionaries. ```bash
subminer app --setup # launch the first-run setup wizard
```
### 3. Mine 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.
### 3. Verify Setup
```bash
subminer doctor # verify mpv, ffmpeg, config, and socket
```
> [!NOTE]
> On Windows, use `bun run subminer doctor` or run `SubMiner.exe` directly — first-run setup will guide you through dependency checks.
### 4. Mine
```bash ```bash
subminer video.mkv # play video with overlay subminer video.mkv # play video with overlay
@@ -240,6 +276,8 @@ subminer stats -b # stats daemon in background
subminer stats -s # stop background stats daemon subminer stats -s # stop background stats daemon
``` ```
On **Windows**, the `subminer` script must be run with `bun run subminer` (e.g. `bun run subminer video.mkv`). The recommended alternative is the **SubMiner mpv** shortcut (created during setup) or `SubMiner.exe --launch-mpv`. Drag a video file onto the shortcut to play it, or double-click it to open mpv with SubMiner's defaults.
## Documentation ## Documentation
Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)** Full guides on configuration, Anki setup, Jellyfin, immersion tracking, and more: **[docs.subminer.moe](https://docs.subminer.moe)**
@@ -0,0 +1,34 @@
---
id: TASK-285
title: Rename anime visibility filter heading to title visibility
status: Done
assignee:
- codex
created_date: '2026-04-10 00:00'
updated_date: '2026-04-10 00:00'
labels:
- stats
- ui
- bug
milestone: m-1
dependencies: []
references:
- stats/src/components/trends/TrendsTab.tsx
- stats/src/components/trends/TrendsTab.test.tsx
priority: low
ordinal: 200000
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Align the library cumulative trends filter UI with the new terminology by renaming the hardcoded anime visibility heading to title visibility.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The trends filter heading uses `Title Visibility`
- [x] #2 The component behavior and props stay unchanged
- [x] #3 A regression test covers the rendered heading text
<!-- AC:END -->
@@ -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,37 @@
---
id: TASK-351
title: Remove legacy global mpv plugin from setup
status: Done
assignee: []
created_date: '2026-05-12 19:57'
updated_date: '2026-05-12 20:03'
labels:
- setup
- mpv-plugin
- launcher
- windows
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Add first-run setup support for detecting all legacy SubMiner mpv plugin auto-load entries and removing them via the OS trash after user confirmation, so regular mpv stops loading SubMiner while SubMiner-managed playback can use runtime plugin loading.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Setup detects all SubMiner mpv auto-load candidates under normal mpv scripts directories and Windows portable_config scripts directories.
- [x] #2 Setup displays detected legacy plugin paths and offers a Remove legacy mpv plugin action.
- [x] #3 Removal uses Electron shell.trashItem for detected script files/directories and never permanently deletes as fallback.
- [x] #4 script-opts/subminer.conf is not removed by the legacy plugin removal action.
- [x] #5 Partial trash failures report exact failed paths and keep legacy plugin warning visible.
- [x] #6 Successful removal refreshes setup status and reports that regular mpv will no longer load SubMiner while SubMiner-managed playback keeps working.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented setup detection for all legacy SubMiner mpv auto-load candidates in normal and portable mpv script directories, added a confirmed Remove legacy mpv plugin action that uses Electron shell.trashItem only, preserves script-opts/subminer.conf, reports exact partial failures, and refreshes setup status after successful removal. Added focused tests plus changelog/docs updates.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,39 @@
---
id: TASK-352
title: Inject bundled mpv plugin for managed launches
status: Done
assignee: []
created_date: '2026-05-12 20:06'
updated_date: '2026-05-12 20:15'
labels:
- launcher
- mpv-plugin
- windows
- setup
dependencies: []
references:
- app-managed-mpv-runtime-plugin-plan.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Implement app-managed mpv runtime plugin loading so SubMiner-managed launcher and Windows mpv shortcut launches do not require a globally installed mpv plugin, while installed legacy/global plugins are detected and take precedence until removed.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launcher-managed mpv launch injects the bundled plugin when no installed SubMiner mpv plugin is detected.
- [x] #2 Launcher idle/Jellyfin mpv launch follows the same bundled-vs-installed plugin policy.
- [x] #3 Windows SubMiner mpv shortcut launch skips bundled injection when an installed plugin is detected and injects bundled plugin otherwise.
- [x] #4 First-run setup no longer requires global mpv plugin installation to finish; plugin install remains optional compatibility action.
- [x] #5 Runtime plugin path resolution is test-covered and reports a clear failure when no bundled plugin path is available and no installed plugin exists.
- [x] #6 Release docs/changelog explain that managed launches no longer require global plugin installation and legacy plugin removal switches to bundled runtime loading.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Implemented app-managed mpv runtime plugin policy. Launcher-managed playback and idle/Jellyfin mpv startup now inject the bundled plugin when no global SubMiner plugin is detected, and keep using/logging the installed plugin when one is present. Windows SubMiner mpv shortcut launches use the same installed-vs-bundled policy while still passing SubMiner script opts. First-run setup no longer requires global plugin installation to finish, keeps legacy install as optional compatibility, and documents/removes legacy global plugin files via OS trash.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,37 @@
---
id: TASK-353
title: Remove Makefile global mpv plugin installer
status: Done
assignee: []
created_date: '2026-05-12 14:07'
updated_date: '2026-05-12 14:07'
labels:
- launcher
- mpv-plugin
- docs
dependencies: []
references:
- app-managed-mpv-runtime-plugin-plan.md
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Remove the legacy Makefile path that installs SubMiner into mpv's global scripts directory, including the Windows config rewrite script hook, because managed playback now injects the bundled runtime plugin.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Makefile no longer exposes an `install-plugin` target or help entry.
- [x] #2 Windows install no longer delegates to global mpv plugin installation.
- [x] #3 The obsolete config rewrite bun script is removed when no longer referenced.
- [x] #4 Docs no longer tell users to run `make install-plugin`.
- [x] #5 Regression coverage prevents reintroducing the target or script hook.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Removed the legacy global mpv plugin install target from the Makefile, removed the obsolete Windows config rewrite script, updated docs to describe bundled runtime plugin loading instead of separate installation, and removed the setup window's legacy install action while keeping legacy plugin removal available.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,38 @@
---
id: TASK-354
title: Show legacy mpv plugin removal before managed playback
status: Done
assignee: []
created_date: '2026-05-12 14:30'
updated_date: '2026-05-12 14:35'
labels:
- launcher
- mpv-plugin
- setup
- windows
dependencies: []
references:
- app-managed-mpv-runtime-plugin-plan.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When SubMiner-managed playback detects a legacy global SubMiner mpv plugin, show the removal UI before mpv starts so users can optionally trash the legacy files and then launch with the bundled runtime plugin.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launcher playback opens first-run setup before mpv starts when legacy global plugin files are detected, even if setup is already completed.
- [x] #2 Launcher playback resumes with bundled runtime plugin after the legacy plugin is removed.
- [x] #3 Windows mpv shortcut/app launch prompts before spawning mpv and re-detects after removal so bundled injection is used.
- [x] #4 Users can continue without removal; removal remains optional.
- [x] #5 Regression coverage prevents bypassing the removal prompt due to completed setup or installed-plugin detection.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Managed launcher playback now checks for legacy global SubMiner mpv plugin files before choosing/loading a video, opens first-run setup even when setup is already complete, and waits for removal or explicit user continuation before starting mpv. Windows managed mpv launches now show a pre-launch removal dialog, move detected legacy files to the OS trash on confirmation, re-detect, and inject the bundled runtime plugin after successful removal.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,62 @@
---
id: TASK-279
title: Fix Linux AppImage child-process libffmpeg resolution
status: Done
assignee:
- '@codex'
created_date: '2026-04-05 17:17'
updated_date: '2026-04-05 17:56'
labels: []
dependencies: []
references:
- 'https://github.com/ksyasuda/SubMiner/issues/41'
documentation:
- docs/workflow/verification.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Fix the Linux AppImage packaging so Chromium child processes relaunched from the bundled binary can resolve the packaged libffmpeg shared library and SubMiner starts cleanly instead of crash-looping on network-service restarts.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Linux AppImage packaging ensures bundled Chromium child processes can resolve the packaged libffmpeg shared library during relaunch.
- [x] #2 Regression coverage exercises the Linux packaging/build configuration that provides the AppImage shared-library path.
- [x] #3 Release notes/changelog reflect the Linux AppImage startup fix.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused regression tests for Linux release packaging that assert the build config invokes an `afterPack` hook and that the hook stages bundled `libffmpeg.so` into `usr/lib` for AppImage runtime lookup.
2. Implement a small electron-builder `afterPack` hook that runs only for Linux, copies `libffmpeg.so` from the packaged app root into `usr/lib`, and no-ops when the source library is absent.
3. Wire the hook into `package.json` build config and add a changelog fragment for the Linux AppImage startup fix.
4. Run the focused test lane first, then the default handoff gate because the change touches release-sensitive packaging behavior.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Chose a repo-local electron-builder `afterPack` hook instead of patching/forking `electron-builder`. The hook copies bundled `libffmpeg.so` from the packaged Linux app root into `usr/lib`, matching the AppImage runtime's existing `LD_LIBRARY_PATH` search path.
Added regression coverage for both config wiring (`src/release-workflow.test.ts`) and the hook behavior (`scripts/electron-builder-after-pack.test.ts`), then wired the new script test into `test:fast` so the maintained lane keeps exercising the fix.
Verification passed: `bun test scripts/electron-builder-after-pack.test.ts src/release-workflow.test.ts`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`.
Addressed PR #45 CodeRabbit review thread: Linux `afterPack` staging now hard-fails when `libffmpeg.so` is missing instead of silently no-oping. Updated focused hook tests to assert the new failure contract and that `afterPack` propagates Linux staging errors.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added a shared electron-builder `afterPack` hook at `scripts/electron-builder-after-pack.cjs` and wired it into `package.json` so Linux packaging stages the bundled `libffmpeg.so` into `usr/lib` inside the packaged app. This keeps Chromium child relaunches compatible with the AppImage runtime's existing `LD_LIBRARY_PATH` layout without forking or patching upstream `electron-builder`.
Regression coverage now checks both the packaging config and the hook behavior: `src/release-workflow.test.ts` asserts the hook stays wired into release config, and `scripts/electron-builder-after-pack.test.ts` verifies Linux copies `libffmpeg.so` into `usr/lib` while non-Linux and missing-library cases no-op safely. The new script test is included in `test:fast`, and a changelog fragment was added under `changes/fix-appimage-libffmpeg-path.md`.
Verification passed with `bun test scripts/electron-builder-after-pack.test.ts src/release-workflow.test.ts`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, and `bun run test:smoke:dist`.
Follow-up review fix on PR #45: Linux packaging now throws when `libffmpeg.so` is missing from the packaged app root, preventing silent shipment of a broken AppImage. Focused regression coverage was updated so the missing-library case rejects and `afterPack` propagates the failure.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,59 @@
---
id: TASK-280
title: >-
Force launcher-spawned mpv onto X11 when backend resolves to x11 or no
supported Wayland tracker is available
status: Done
assignee:
- codex
created_date: '2026-04-05 21:01'
updated_date: '2026-04-05 21:05'
labels:
- bug
- linux
- launcher
- overlay
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
On Linux Plasma Wayland and similar sessions, `subminer --backend=x11` currently only changes SubMiner's window-tracker override. The launcher still spawns mpv without forcing an X11/XWayland backend, so the X11 tracker cannot find the mpv window and the overlay remains hidden. Update launcher-side mpv spawn behavior so launcher-managed mpv runs under X11 when backend resolves to `x11`, and also when auto detection cannot resolve to a supported Wayland tracker. Preserve existing Hyprland/Sway behavior.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launcher-managed mpv is spawned with X11/XWayland-forcing environment/config when backend resolves to `x11`.
- [x] #2 Linux auto mode falls back to X11/XWayland-forced mpv when no supported Wayland tracker backend is detected.
- [x] #3 Hyprland and Sway launcher flows do not regress to forced X11 mpv.
- [x] #4 Regression tests cover launcher env/backend selection for these Linux cases.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused launcher tests that assert the mpv spawn environment forces X11 when backend resolves to `x11`, and when Linux auto mode cannot use a supported Wayland tracker.
2. Refactor launcher mpv spawn code to compute an mpv-specific environment without changing existing Hyprland/Sway flows.
3. Route all launcher-managed mpv spawns through the new environment helper.
4. Run focused launcher tests, then summarize behavior and any remaining verification gaps.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
User approved scope: force launcher-managed mpv to X11 for explicit `--backend=x11` and for unsupported Linux Wayland auto-detect fallback; preserve Hyprland/Sway behavior.
Implemented launcher-side `buildMpvEnv` to strip Wayland hints and force X11/XWayland for launcher-managed mpv when `--backend=x11`, and for Linux auto mode on unsupported Wayland desktops with an X11 display available. Wired both normal mpv launches and idle detached mpv launches through the helper.
Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpvEnv"` passed; `bun run tsc --noEmit` passed. A broader `bun test launcher/mpv.test.ts` run still hits a pre-existing sandbox-specific failure in `launchAppCommandDetached handles child process spawn errors` because this environment cannot write the default app log path under `/home/sudacode/.config/SubMiner/logs`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated the launcher so mpv gets an X11/XWayland-oriented spawn environment whenever the user explicitly requests `--backend=x11`, and when Linux auto mode is running under an unsupported Wayland desktop that still exposes an X11 display. The new helper reuses the launcher child-process base environment, strips Wayland-specific hints (`WAYLAND_DISPLAY`, Hyprland/Sway markers), and flips `XDG_SESSION_TYPE` to `x11` only for those fallback cases. Both foreground mpv launches and detached idle mpv launches now use the same helper so overlay-tracked playback stays consistent.
Added focused regression coverage in `launcher/mpv.test.ts` for three cases: explicit `x11` forcing, unsupported Wayland auto fallback (for example KDE Plasma Wayland), and preserving native Wayland env for supported Hyprland/Sway auto backends. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpvEnv"` and `bun run tsc --noEmit`. A broader launcher mpv test run still shows an unrelated sandbox write failure for the default app log path in this environment.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,62 @@
---
id: TASK-281
title: Prevent Windows launcher tests from leaking backslash temp files on POSIX
status: Done
assignee:
- codex
created_date: '2026-04-05 21:13'
updated_date: '2026-04-05 21:20'
labels:
- tests
- launcher
- bug
dependencies: []
documentation:
- /home/sudacode/github/SubMiner2/AGENTS.md
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Windows-specific launcher tests in launcher/mpv.test.ts currently create real filesystem entries using path.win32.join(...) with a POSIX mkdtemp base. On Linux/macOS this produces literal backslash-named paths like \\tmp\\subminer-test-win-dir-* inside the repo/worktree, and the existing cleanup only removes the POSIX /tmp base directory. Fix the tests so they still cover Windows path resolution behavior without leaking stray files into the working tree.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Running the Windows findAppBinary tests on a POSIX host does not create new untracked \\tmp\\subminer-test-win-* files in the repository root.
- [x] #2 The Windows launcher tests still validate PATH and install-directory resolution behavior.
- [x] #3 Relevant launcher tests pass after the change.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a regression test in launcher/mpv.test.ts that exercises the Windows findAppBinary cases on a POSIX host and asserts they do not leave new backslash-named temp artifacts in the repository root.
2. Refactor the Windows launcher tests to avoid creating real filesystem paths from path.win32.join(...) on POSIX; keep Windows path assertions via stubs and only create real files with native POSIX paths where needed.
3. Run the targeted launcher tests and confirm no new \\tmp\\subminer-test-win-* artifacts appear in git status.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Investigation: reproduced the leak locally. The source is launcher/mpv.test.ts Windows findAppBinary tests that combine a POSIX mkdtemp base with path.win32.join(...), creating literal backslash-named entries like \\tmp\\subminer-test-win-dir-* in the repo root. Existing cleanup only removes the POSIX /tmp base directory.
User approved implementation plan on 2026-04-05.
Implemented in launcher/mpv.test.ts by replacing the leaky Windows PATH/install-directory helpers with pure fs stubs (access/exists/stat) and fixed Windows path strings instead of creating real path.win32 filesystem entries on POSIX. Added a regression test that snapshots repo-root \\tmp\\subminer-test-win-* artifacts before/after running the Windows cases and asserts no new entries are created.
Verification: `bun test launcher/mpv.test.ts --test-name-pattern 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX|findAppBinary resolves SubMiner.exe on PATH on Windows|findAppBinary resolves a Windows install directory to SubMiner.exe'` passed (3/3). `bun test launcher/mpv.test.ts` still has one unrelated pre-existing sandbox failure in `launchAppCommandDetached handles child process spawn errors` because the test opens `~/.config/SubMiner/logs/app-2026-04-05.log` and hits `EROFS` in this environment.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Reworked the Windows `findAppBinary` tests in `launcher/mpv.test.ts` so they no longer create real backslash-named temp files on POSIX hosts. The PATH and install-directory cases now use synthetic Windows path strings plus `fs.accessSync` / `fs.existsSync` / `fs.statSync` stubs to exercise the same resolver behavior without writing `\\tmp\\subminer-test-win-*` entries into the repository root.
Added a POSIX regression test that snapshots existing repo-root `\\tmp\\subminer-test-win-*` artifacts, runs the Windows path-resolution cases, and asserts the artifact set is unchanged. This catches future regressions where a Windows-path test accidentally writes literal backslash paths on Linux/macOS.
Tests run:
- `bun test launcher/mpv.test.ts --test-name-pattern 'findAppBinary Windows cases do not leak backslash temp artifacts on POSIX|findAppBinary resolves SubMiner.exe on PATH on Windows|findAppBinary resolves a Windows install directory to SubMiner.exe'`
- `bun test launcher/mpv.test.ts` (all relevant `findAppBinary` tests passed; one unrelated existing sandbox failure remains in `launchAppCommandDetached handles child process spawn errors` due `EROFS` opening `~/.config/SubMiner/logs/...`)
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,58 @@
---
id: TASK-282
title: >-
Force launcher-managed mpv to an explicit X11 GPU context when X11 fallback is
active
status: Done
assignee:
- codex
created_date: '2026-04-05 21:14'
updated_date: '2026-04-05 21:15'
labels:
- bug
- linux
- launcher
- mpv
- overlay
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Follow-up to the launcher X11 fallback work: on Plasma Wayland, stripping Wayland env vars alone is not sufficient for launcher-managed mpv. mpv can still fail GPU initialization under the default video output path (`vo/gpu-next`) unless an explicit X11 GPU context is selected. Update launcher-managed mpv startup so X11 fallback mode also appends explicit mpv options for an X11 GPU context, while preserving supported Hyprland/Sway Wayland flows.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launcher-managed mpv appends explicit X11 GPU context args when explicit `--backend=x11` is used on Linux.
- [x] #2 Launcher-managed mpv appends the same explicit X11 GPU context args for Linux auto-mode fallback on unsupported Wayland desktops.
- [x] #3 Supported Hyprland/Sway Wayland flows do not receive the forced X11 GPU context args.
- [x] #4 Regression tests cover the forced-X11 mpv arg selection.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add focused launcher tests for explicit X11 GPU-context arg selection, unsupported Wayland auto fallback, and Hyprland/Sway no-regression cases.
2. Introduce a shared launcher helper that decides when mpv should be forced onto X11 fallback mode.
3. Use that helper to append explicit mpv X11 GPU-context args in both normal and detached idle mpv launch paths.
4. Run focused launcher tests plus TypeScript verification, then record remaining runtime follow-up guidance.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
User runtime log showed `vo/gpu-next` failing with `Failed initializing any suitable GPU context!` under forced-X11 playback, which indicates env forcing alone was insufficient. Selected `--gpu-context=x11egl,x11` as the explicit mpv fallback: prefer X11/EGL, with GLX as a compatibility fallback.
Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` passed. `bun run tsc --noEmit` passed.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added explicit mpv backend args for launcher-managed X11 fallback mode. The launcher now uses a shared `shouldForceX11MpvBackend` decision for both env rewriting and mpv arg selection, so explicit `--backend=x11` and unsupported Linux Wayland auto fallback both append `--gpu-context=x11egl,x11` while still stripping Wayland env hints. This preserves supported Hyprland/Sway native Wayland flows and makes the X11 fallback more explicit for mpv's GPU initialization path.
Wired the new X11 GPU-context args into both the normal playback launch path and the detached idle mpv launch path. Added focused regression coverage for explicit `x11`, Plasma-style unsupported Wayland auto fallback, and Hyprland/Sway no-regression behavior. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` and `bun run tsc --noEmit`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,56 @@
---
id: TASK-283
title: Force launcher-managed X11 fallback to mpv vo=gpu with OpenGL on Linux
status: Done
assignee:
- codex
created_date: '2026-04-05 21:19'
updated_date: '2026-04-05 21:20'
labels:
- bug
- linux
- launcher
- mpv
- overlay
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Follow-up to explicit X11 gpu-context forcing: Plasma Wayland runtime logs still show mpv using `vo/gpu-next` and failing to initialize any suitable GPU context under launcher-managed X11 fallback. Update launcher-managed mpv X11 fallback mode to force a more compatible renderer stack: `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`, while preserving supported native Wayland flows and allowing explicit user mpv args to override later on the command line.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launcher-managed mpv appends `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11` when explicit `--backend=x11` is used on Linux.
- [x] #2 Launcher-managed mpv appends the same renderer args for Linux auto-mode fallback on unsupported Wayland desktops.
- [x] #3 Supported Hyprland/Sway Wayland flows do not receive the forced X11 renderer args.
- [x] #4 Regression tests cover the forced-X11 renderer arg selection.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Tighten launcher tests so forced-X11 renderer args require `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`.
2. Update the shared launcher X11-fallback helper to return the full renderer arg stack for explicit `x11` and unsupported Wayland auto fallback.
3. Re-run focused launcher env/backend tests and TypeScript verification.
4. Hand back with retry instructions and next debugging branch if runtime still fails.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Runtime log still showed `[vo/gpu-next] Failed initializing any suitable GPU context!`, which meant forcing only the context was not enough. Updated the fallback to force the classic OpenGL renderer path too: `--vo=gpu --gpu-api=opengl --gpu-context=x11egl,x11`.
Verification: `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` passed. `bun run tsc --noEmit` passed.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Updated launcher-managed X11 fallback mode to force a more compatible mpv renderer stack on Linux: `--vo=gpu`, `--gpu-api=opengl`, and `--gpu-context=x11egl,x11`. This applies both to explicit `--backend=x11` and to unsupported Wayland auto fallback, while supported Hyprland/Sway Wayland sessions still keep their native path. The renderer args are still inserted before user-supplied `--args`, so an explicit user override can win later on the command line if needed.
Adjusted regression coverage to require the full renderer stack for forced-X11 mode and verified the helper behavior with focused launcher tests plus TypeScript compilation. Verification completed with `bun test launcher/mpv.test.ts --test-name-pattern "buildMpv(Env|BackendArgs)"` and `bun run tsc --noEmit`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,32 @@
---
id: TASK-284
title: Fix CI changelog fragment requirement for launcher X11 MPV fallback change
status: Done
assignee: []
created_date: '2026-04-05 22:21'
updated_date: '2026-04-05 22:22'
labels:
- ci
- changelog
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Current CI is failing in `changelog:pr-check` because PR changes release-relevant files but does not include a required entry under `changes/` and lacks `skip-changelog` label. Add a release fragment describing the behavioral change and verify the CI gate passes.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Add a correctly formatted changelog fragment under `changes/` for the current change
- [x] #2 Run the local changelog PR check (or equivalent) with passing result
- [x] #3 Run required CI gate commands after change and confirm no regressions
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Added `changes/2026.04.05-mpv-x11-fallback.md` with launcher release-note metadata so `changelog:pr-check` can pass. Verified local CI gate commands: `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist` all passed. Ran manual PR changelog verification by invoking `verifyPullRequestChangelog` with current git diff plus the new fragment and confirmed it passes.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,54 @@
---
id: TASK-285
title: Investigate inconsistent mpv y-t overlay toggle after menu toggle
status: To Do
assignee: []
created_date: '2026-04-07 22:55'
updated_date: '2026-04-07 22:55'
labels:
- bug
- overlay
- keyboard
- mpv
dependencies: []
references:
- plugin/subminer/process.lua
- plugin/subminer/ui.lua
- src/renderer/handlers/keyboard.ts
- src/main/runtime/autoplay-ready-gate.ts
- src/core/services/overlay-window-input.ts
- backlog/tasks/task-248 - Fix-macOS-visible-overlay-toggle-getting-immediately-restored.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
User report: toggling the visible overlay with mpv `y-t` is inconsistent. After manually toggling through the `y-y` menu, `y-t` may allow one hide, but after toggling back on it can stop hiding the overlay again, forcing the user back into the menu path.
Initial assessment:
- no active backlog item currently tracks this exact report
- nearest prior work is `TASK-248`, which fixed a macOS-specific visible-overlay restore bug and is marked done
- current targeted regressions for the old fix surface pass, including plugin ready-signal suppression, focused-overlay `y-t` proxy dispatch, autoplay-ready gate deduplication, and blur-path restacking guards
This should be treated as a fresh investigation unless reproduction proves it is the same closed macOS issue resurfacing on the current build.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 Reproduce the reported `y-t` / `y-y` inconsistency on the affected platform and identify the exact event sequence
- [ ] #2 Determine whether the failure is in mpv plugin command dispatch, focused-overlay key forwarding, or main-process visible-overlay state transitions
- [ ] #3 Fix the inconsistency so repeated hide/show/hide cycles work from `y-t` without requiring menu recovery
- [ ] #4 Add regression coverage for the reproduced failing sequence
- [ ] #5 Record whether this is a regression of `TASK-248` or a distinct bug
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Reproduce the report with platform/build details and capture whether the failing `y-t` press originates in raw mpv or the focused overlay y-chord proxy path.
2. Trace visible-overlay state mutations across plugin toggle commands, autoplay-ready callbacks, and main-process visibility/window blur handling.
3. Patch the narrowest failing path and add regression coverage for the exact hide/show/hide sequence.
4. Re-run targeted plugin, overlay visibility, overlay window, and renderer keyboard suites before broader verification.
<!-- SECTION:PLAN:END -->
@@ -0,0 +1,63 @@
---
id: TASK-286
title: 'Assess and address PR #49 CodeRabbit review follow-ups'
status: Done
assignee:
- codex
created_date: '2026-04-11 18:55'
updated_date: '2026-04-11 22:40'
labels:
- bug
- code-review
- windows
- overlay
dependencies: []
references:
- src/main/runtime/config-hot-reload-handlers.ts
- src/renderer/handlers/keyboard.ts
- src/renderer/handlers/mouse.ts
- vendor/subminer-yomitan
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the current PR #49 review round and resolve the actionable CodeRabbit findings on the Windows update branch.
Focus areas include the renderer mouse interaction fix, config hot-reload keyboard state, and any other review items that still apply after verifying the current branch state.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All actionable CodeRabbit comments on PR #49 are either fixed or shown to be obsolete with evidence.
- [x] #2 Regression tests are added or updated for any behavior change that could regress.
- [x] #3 The branch passes the repo's relevant verification checks for the touched areas.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Pull the current unresolved CodeRabbit review threads for PR #49 and cluster them into still-actionable fixes versus obsolete/nit-only items.
2. For each still-actionable behavior bug, add or extend the narrowest failing test first in the touched suite before changing production code.
3. Implement the minimal fixes across the affected runtime, renderer, plugin, IPC, and Windows tracker files, keeping each change traceable to the review thread.
4. Run targeted verification for the touched areas, update task notes with assessment results, and capture which review comments were fixed versus assessed as obsolete or deferred nitpicks.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Assessed PR #49 CodeRabbit threads. Fixed the real regressions in first-run CLI gating, IPC session-action validation, renderer controller-modal lifecycle notifications, async subtitle-sidebar toggle guarding, plugin config-dir resolution priority, prerelease artifact upload failure handling, immersion tracker lazy startup, win32 z-order error handling, and Windows socket-aware mpv matching.
Review assessment: the overlay-shortcut lifecycle comment is obsolete for the current architecture because overlay-local shortcuts are intentionally handled through the local fallback path and the runtime only tracks configured-state/cleanup. Refactor-only nit comments for splitting `scripts/build-changelog.ts` and `src/core/services/session-bindings.ts` were left as follow-up quality work, not behavior bugs in this PR round.
Verification: `bun test src/main/runtime/first-run-setup-service.test.ts src/core/services/session-bindings.test.ts src/core/services/app-ready.test.ts src/core/services/ipc.test.ts src/renderer/handlers/keyboard.test.ts src/main/overlay-runtime.test.ts src/window-trackers/mpv-socket-match.test.ts`, `bun test src/window-trackers/windows-tracker.test.ts`, `bun run typecheck`, `lua scripts/test-plugin-lua-compat.lua`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Assessed the current CodeRabbit round on PR #49 and addressed the still-valid behavior issues rather than blanket-applying every bot suggestion. The branch now treats the new session/stats CLI flags as explicit startup commands during first-run setup, validates the new session actions through IPC, points session-binding command diagnostics at the correct config field, keeps immersion tracker startup lazy until later runtime triggers, and notifies overlay modal lifecycle state when controller-select/debug are opened from local keyboard bindings. I also switched the subtitle-sidebar IPC callback to the async guarded path so promise rejections feed renderer recovery instead of being dropped.
On the Windows/plugin side, the mpv plugin now prefers config-file matches before falling back to an existing config directory, prerelease workflow uploads fail if expected Linux/macOS artifacts are missing, the Win32 z-order bind path now validates the `GetWindowLongW` call for the window above mpv, and the Windows tracker now passes the target socket path into native polling and filters mpv instances by command line so multiple sockets can be distinguished on Windows. Added/updated regression coverage for first-run gating, IPC validation, session-binding diagnostics, controller modal lifecycle notifications, modal ready-listener dispatch, and socket-path matching. Verification run: `bun run typecheck`, the targeted Bun test suites for the touched areas, `bun test src/window-trackers/windows-tracker.test.ts`, and `lua scripts/test-plugin-lua-compat.lua`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,61 @@
---
id: TASK-286.1
title: 'Assess and address PR #49 subsequent CodeRabbit review round'
status: Done
assignee: []
created_date: '2026-04-11 23:14'
updated_date: '2026-04-11 23:16'
labels:
- bug
- code-review
- windows
- release
dependencies: []
references:
- .github/workflows/prerelease.yml
- src/window-trackers/mpv-socket-match.ts
- src/window-trackers/win32.ts
- src/core/services/overlay-shortcut.ts
parent_task_id: TASK-286
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the next unresolved CodeRabbit review threads on PR #49 after commit 9ce5de2f and resolve the still-valid follow-up issues without reopening already-assessed stale comments.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All still-actionable CodeRabbit comments in the latest PR #49 round are fixed or explicitly shown stale with evidence.
- [x] #2 Regression coverage is added or updated for any behavior-sensitive fix in workflow or Windows socket matching.
- [x] #3 Relevant verification passes for the touched workflow, tracker, and shared matcher changes.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Verify the five unresolved CodeRabbit threads against current branch state and separate still-valid bugs from stale comments.
2. Add or extend the narrowest failing tests for exact socket-path matching and prerelease workflow invariants before changing production code.
3. Implement minimal fixes in the prerelease workflow and Windows socket matching/cache path, leaving stale comments documented with evidence instead of forcing no-op changes.
4. Run targeted verification, then record the fixed-vs-stale assessment and close the subtask.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Assessed five unresolved PR #49 threads after 9ce5de2f. Fixed prerelease workflow cache keys to include `runner.arch`, changed prerelease publishing to validate artifacts before release creation/edit and only undraft after uploads complete, tightened Windows socket matching to require exact argument boundaries, and stopped memoizing null command-line lookup misses in the Win32 cache path.
Stale assessment: the `src/core/services/overlay-shortcut.ts` thread is still obsolete. Current code at `registerOverlayShortcuts()` returns `hasConfiguredOverlayShortcuts(shortcuts)`, not `false`, and the overlay-local handling remains intentionally driven by local fallback dispatch rather than global registration in this runtime path.
Verification: `bun test src/prerelease-workflow.test.ts src/window-trackers/mpv-socket-match.test.ts`, `bun test src/window-trackers/windows-tracker.test.ts src/prerelease-workflow.test.ts src/window-trackers/mpv-socket-match.test.ts`, `bun run typecheck`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Handled the next CodeRabbit round on PR #49 by fixing the still-valid prerelease workflow and Windows socket-matching issues while documenting the stale overlay-shortcut comment instead of forcing a no-op code change. The prerelease workflow now scopes all dependency caches by `runner.arch`, validates the final artifact set before touching the GitHub release, creates/edits the prerelease as a draft during uploads, and only flips `--draft=false` after all assets succeed. On Windows, socket matching now requires an exact `--input-ipc-server` argument boundary so `subminer-1` no longer matches `subminer-10`, and transient PowerShell/CIM misses no longer get cached forever as null command lines.
Regression coverage was added for the workflow invariants and exact socket matching. Verification passed with targeted prerelease workflow tests, Windows tracker tests, socket-matcher tests, and `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,49 @@
---
id: TASK-286.2
title: 'Assess and address PR #49 next CodeRabbit review round'
status: Done
assignee: []
created_date: '2026-04-12 02:50'
updated_date: '2026-04-12 02:52'
labels:
- bug
- code-review
- release
- testing
dependencies: []
references:
- .github/workflows/prerelease.yml
- src/prerelease-workflow.test.ts
- src/core/services/overlay-shortcut.ts
parent_task_id: TASK-286
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the next unresolved CodeRabbit review threads on PR #49 after commit 62ad77dc and resolve the still-valid follow-up issues while documenting stale repeats.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All still-actionable CodeRabbit comments in the latest PR #49 round are fixed or explicitly shown stale with evidence.
- [x] #2 Regression coverage is updated for any workflow or test changes made in this round.
- [x] #3 Relevant verification passes for the touched workflow and prerelease test changes.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Assessed latest unresolved CodeRabbit round on PR #49. `src/core/services/overlay-shortcut.ts` comment is stale: `registerOverlayShortcuts()` returns `hasConfiguredOverlayShortcuts(shortcuts)`, so runtime registration is not hard-coded false.
Added exact, line-ending-agnostic prerelease tag trigger assertions in `src/prerelease-workflow.test.ts` and a regression asserting `bun run test:env` sits in the prerelease quality gate before source coverage.
Updated `.github/workflows/prerelease.yml` quality-gate to run `bun run test:env` after `bun run test:fast`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Assessed the latest CodeRabbit round for PR #49. Left the `overlay-shortcut.ts` thread open as stale with code evidence, tightened prerelease workflow trigger coverage, and added the missing `test:env` step to the prerelease quality gate. Verification: `bun test src/prerelease-workflow.test.ts`; `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,48 @@
---
id: TASK-286.3
title: 'Assess and address PR #49 latest CodeRabbit review round'
status: Done
assignee: []
created_date: '2026-04-12 03:08'
updated_date: '2026-04-12 03:09'
labels:
- bug
- code-review
- testing
dependencies: []
references:
- 'PR #49'
- .github/workflows/prerelease.yml
- src
parent_task_id: TASK-286
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Track the newest unresolved CodeRabbit review threads on PR #49 after commit 942c1649, fix the still-valid issues, verify them, and push the branch update.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 All still-actionable CodeRabbit comments in the newest PR #49 round are fixed or explicitly identified stale with evidence.
- [x] #2 Regression coverage is added or updated for behavior touched in this round.
- [x] #3 Relevant verification passes before commit and push.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Fetched the newest unresolved CodeRabbit threads for PR #49 after commit `942c1649`; only one unresolved actionable thread remained, on prerelease checksum output using repo-relative paths instead of asset basenames.
Added regression coverage in `src/prerelease-workflow.test.ts` and `src/release-workflow.test.ts` asserting checksum generation truncates to asset basenames and no longer writes the raw `sha256sum "${files[@]}" > release/SHA256SUMS.txt` form.
Updated both `.github/workflows/prerelease.yml` and `.github/workflows/release.yml` checksum generation steps to iterate over the `files` array and write `SHA256 basename` lines into `release/SHA256SUMS.txt`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Resolved the latest CodeRabbit round for PR #49 by fixing checksum generation to emit basename-oriented `SHA256SUMS.txt` entries in both prerelease and release workflows, with matching regression coverage. Verification: `bun test src/prerelease-workflow.test.ts src/release-workflow.test.ts`; `bun run typecheck`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,44 @@
---
id: TASK-287
title: Restore Lua parser compatibility for mpv plugin modules
status: Done
assignee: []
created_date: '2026-04-11 21:25'
updated_date: '2026-04-11 21:29'
labels:
- bug
- mpv-plugin
- lua
dependencies: []
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Users with Lua runtimes that do not accept the current `goto continue` pattern in the mpv plugin should be able to load the plugin without syntax errors. Remove the parser-incompatible control-flow usage from the affected plugin modules without changing plugin behavior.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 The mpv plugin source no longer relies on parser-incompatible `goto continue` labels in the affected Lua modules.
- [x] #2 Automated coverage fails on the old parser-incompatible source and passes once the compatibility fix is in place.
- [x] #3 Existing plugin start/gate verification still passes after the compatibility fix.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Reused existing local cleanups in `plugin/subminer/hover.lua` and `plugin/subminer/environment.lua` to remove `goto continue` / `::continue::` control flow without behavior changes.
Added `scripts/test-plugin-lua-compat.lua` and wired it into `test:plugin:src`; the regression checks reject the legacy pattern structurally and verify parse success with `luajit` when available.
Verification run on 2026-04-11: `lua scripts/test-plugin-lua-compat.lua` ✅, `bun run test:plugin:src` ✅, `bun run changelog:lint` ✅, `bun run typecheck` ✅, `bun run test:env` ✅, `bun run build` ✅, `bun run test:smoke:dist` ✅.
`bun run test:fast` remains red for unrelated existing immersion-tracker assertions in `src/core/services/immersion-tracker/__tests__/query-split-modules.test.ts` and `src/core/services/immersion-tracker/__tests__/query.test.ts` (`tsMs`/`lastWatchedMs` observed as `-2147483648`).
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Removed parser-incompatible `goto continue` usage from the affected mpv Lua plugin modules, added a dedicated Lua compatibility regression script to the plugin test lane, and added a changelog fragment for the user-visible fix. Requested plugin verification is green; unrelated existing `test:fast` immersion-tracker failures remain outside this task.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,42 @@
---
id: TASK-288
title: Stabilize immersion-tracker CI timestamp handling under libsql/Bun
status: Done
assignee: []
created_date: '2026-04-11 21:34'
updated_date: '2026-04-11 21:43'
labels:
- bug
- ci
- immersion-tracker
dependencies: []
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
`bun run test:fast` is currently failing because large millisecond timestamps are not handled safely through the libsql/Bun path. Fix timestamp parsing/storage so lifetime/library and session-event queries return correct wall-clock values in CI and runtime.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Large wall-clock timestamps round-trip correctly through immersion-tracker lifetime/library queries under the repo's Bun/libsql runtime.
- [x] #2 Session-event timestamps round-trip correctly for real wall-clock values used by runtime event inserts.
- [x] #3 Targeted immersion-tracker regression coverage passes, and the previously failing `test:fast` lane no longer fails on these timestamp assertions.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Root cause split in two places: Bun/libsql corrupts large millisecond timestamp strings when coerced through `Number(...)`, and `imm_session_events.ts_ms` being `INTEGER` let runtime event inserts/readbacks return `-2147483648` on CI/runtime.
Fix shipped by parsing timestamp strings without the broken `Number(largeString)` path, migrating `imm_session_events.ts_ms` to `TEXT`, ordering/retention queries via `CAST(ts_ms AS REAL)`, and avoiding `Number(currentMs)` when reusing already-normalized timestamp strings.
Added regression coverage for both real runtime event inserts and schema migration/repair of previously truncated session-event rows.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed immersion-tracker timestamp handling under Bun/libsql so large wall-clock millisecond values survive runtime writes, query reads, and schema migration. `bun run test:fast`, `bun run typecheck`, `bun run test:env`, `bun run build`, `bun run test:smoke:dist`, and `bun run changelog:lint` all pass after the patch.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,33 @@
---
id: TASK-289
title: Finish current windows-qol rebase
status: Done
assignee: []
created_date: '2026-04-11 22:07'
updated_date: '2026-04-11 22:08'
labels:
- maintenance
- rebase
dependencies: []
references:
- /home/sudacode/projects/japanese/SubMiner
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Resolve the in-progress rebase on `windows-qol` and ensure the branch lands cleanly.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Interactive rebase completes without conflicts.
- [x] #2 Working tree is clean after the rebase finishes.
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Completed the interactive rebase on `windows-qol` and resolved the transient editor-blocked `git rebase --continue` step. Branch now rebased cleanly onto `49e46e6b`.
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -0,0 +1,56 @@
---
id: TASK-290
title: Cut stable release v0.12.0 on main
status: Done
assignee:
- codex
created_date: '2026-04-12 04:47'
updated_date: '2026-04-12 04:51'
labels: []
dependencies: []
documentation:
- docs/RELEASING.md
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Prepare the main branch for the stable SubMiner v0.12.0 release by applying the release-version updates, formatting changes required by the branch state, and rerunning the full release verification gate.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Main branch version and stable release metadata are updated for v0.12.0.
- [x] #2 Required formatting changes for the release candidate tree are applied and verified.
- [x] #3 The documented release verification gate passes locally and any remaining push or tag prerequisites are documented.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Audit main-branch release state: package version, release artifacts, current CI status, and current formatting debt.
2. Apply required formatting fixes to the files reported by `bun run format:check:src` and verify the formatting lane passes.
3. Update the package version to 0.12.0 and generate stable release metadata (`CHANGELOG.md`, `release/release-notes.md`, `docs-site/changelog.md`) using the documented release workflow.
4. Run the full local release gate on main (`changelog:lint`, `changelog:check --version 0.12.0`, `verify:config-example`, `typecheck`, `test:fast`, `test:env`, `build`, `docs:test`, `docs:build`, plus dist smoke) and document any remaining tag/push prerequisites.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Applied Prettier to all 39 files reported by `bun run format:check:src` on main and verified the formatting lane now passes.
Reapplied the stable changelog build entrypoint fix on main: added `writeStableReleaseArtifacts`, covered it with a focused regression test, and updated `package.json` so `changelog:build` forwards `--version` and `--date` through a single `build-release` command.
Verified the formatted mainline release tree with `bun run changelog:lint`, `bun run changelog:check --version 0.12.0`, `bun run verify:config-example`, `bun run typecheck`, `bun run test:fast`, `bun run test:env`, `bun run build`, `bun run docs:test`, `bun run docs:build`, and `bun run test:smoke:dist`; all passed.
Remote main CI also completed successfully for `Windows update (#49)` after the local release-prep pass. Remaining operational steps are commit/tag/push only.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Prepared `main` for the stable `v0.12.0` cut. Formatted the previously failing source files so `bun run format:check:src` is now clean, bumped `package.json` from `0.12.0-beta.3` to `0.12.0`, and generated the stable release artifacts with the explicit local cut date `2026-04-11`, which consumed the pending changelog fragments into `CHANGELOG.md`, `docs-site/changelog.md`, and `release/release-notes.md`.
Also reintroduced the release-script fix on main: the old `changelog:build` package script still used `build && docs`, which can drop `--version/--date` on the first step. Added a focused regression test in `scripts/build-changelog.test.ts`, implemented `writeStableReleaseArtifacts` in `scripts/build-changelog.ts`, and switched `package.json` to `build-release` so release flags propagate correctly. Verification on the final tree passed for formatting, changelog lint/check, config example verification, typecheck, fast tests, env tests, build, docs tests/build, dist smoke, and remote main CI. The branch is release-ready pending commit, tag, and push.
<!-- 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,38 @@
---
id: TASK-355
title: Unify AniList API throttling across dictionary stats and tracking
status: In Progress
assignee: []
created_date: '2026-05-12 21:49'
updated_date: '2026-05-13 01:21'
labels:
- anilist
- rate-limit
- bug
dependencies: []
references:
- 'https://docs.anilist.co/guide/rate-limiting'
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Audit and fix AniList GraphQL usage so character dictionary generation, stats search/cover art, and post-watch tracking share conservative request pacing and honor AniList rate-limit response headers. Current logs do not show 429s, but source has separate/unthrottled call paths and repeated dictionary lookup failures for the same title.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 All AniList GraphQL call paths use a shared/conservative limiter or equivalent pacing before requests.
- [ ] #2 429 responses honor Retry-After/X-RateLimit-Reset and do not continue hammering the API.
- [x] #3 Stats AniList search endpoint no longer bypasses the AniList rate limiter.
- [x] #4 Post-watch tracking no longer bypasses the AniList rate limiter.
- [x] #5 Focused regression tests cover limiter use for stats search and post-watch tracking, plus existing limiter behavior remains green.
- [x] #6 Stats cover-art lookup reuses already stored AniList cover data for the same anime before issuing another AniList API request.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented stats cover-art cache reuse across videos in the same anime before any AniList/image fetch. Added limiter plumbing for stats manual AniList search and post-watch tracking; both paths now call acquire before GraphQL and record response headers afterward. Character dictionary still uses its existing local pacing and remains follow-up work for fully shared limiter/header handling.
<!-- SECTION:NOTES:END -->
@@ -0,0 +1,58 @@
---
id: TASK-356
title: Close launcher-started background app when mpv exits
status: Done
assignee:
- codex
created_date: '2026-05-13 01:37'
updated_date: '2026-05-13 01:40'
labels:
- bug
- launcher
- mpv
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
When SubMiner is started through the launcher-managed mpv flow, closing the mpv window should also close the background Electron app instead of leaving it running in the tray. Preserve intentional tray/background behavior for normal app startup.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Launcher-managed mpv sessions signal or otherwise cause the spawned background app to quit when the mpv process exits.
- [x] #2 Normal background/tray startup remains available when SubMiner is launched without a launcher-managed playback session.
- [x] #3 A regression test covers the launcher mpv close/shutdown behavior.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1. Add a launcher command regression test for mpv plugin auto-start playback: no direct startOverlay call, mpv exits, launcher marks the session as managed and runs cleanup.
2. Add a small launcher mpv lifecycle helper to mark a SubMiner app session as launcher-managed when the launcher relies on plugin auto-start.
3. Wire playback-command to call that helper only for launcher-managed playback paths where mpv plugin auto-start is expected.
4. Run the focused launcher tests, then update TASK-356 acceptance criteria/notes.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Implemented launcher ownership marking for plugin-auto-start playback sessions. Direct startOverlay already marks launcher ownership; the plugin-auto-start branch now does the same before waiting for mpv exit, so existing cleanup sends the app --stop when mpv closes. Added regression coverage in launcher/commands/playback-command.test.ts. Verification: bun test launcher/commands/playback-command.test.ts; bun test launcher/mpv.test.ts launcher/commands/playback-command.test.ts; bun run test:launcher:src; bun run typecheck. Typecheck initially caught a nullable test fixture assignment and passed after fixing it.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Summary:
- Added markOverlayManagedByLauncher() to centralize launcher ownership tracking for SubMiner app sessions.
- Mark plugin-auto-start playback sessions as launcher-managed, so closing mpv triggers existing cleanup and stops the background app instead of leaving it in the tray.
- Added a regression test covering mpv exit after launcher-managed plugin auto-start playback.
Tests:
- bun test launcher/commands/playback-command.test.ts
- bun test launcher/mpv.test.ts launcher/commands/playback-command.test.ts
- bun run test:launcher:src
- bun run typecheck
<!-- SECTION:FINAL_SUMMARY:END -->
+3
View File
@@ -12,6 +12,7 @@
"commander": "^14.0.3", "commander": "^14.0.3",
"hono": "^4.12.7", "hono": "^4.12.7",
"jsonc-parser": "^3.3.1", "jsonc-parser": "^3.3.1",
"koffi": "^2.15.6",
"libsql": "^0.5.22", "libsql": "^0.5.22",
"ws": "^8.19.0", "ws": "^8.19.0",
}, },
@@ -478,6 +479,8 @@
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"koffi": ["koffi@2.15.6", "", {}, "sha512-WQBpM5uo74UQ17UpsFN+PUOrQQg4/nYdey4SGVluQun2drYYfePziLLWdSmFb4wSdWlJC1aimXQnjhPCheRKuw=="],
"lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="],
"libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="], "libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="],
@@ -0,0 +1,5 @@
type: changed
area: setup
- SubMiner-managed mpv launches now inject the bundled mpv plugin when no global SubMiner plugin is installed, setup can remove detected legacy global plugin files via the OS trash, and legacy global plugin install entrypoints have been removed so regular mpv playback stays unaffected.
- Managed playback now surfaces the legacy plugin removal prompt before mpv starts, allowing users to trash the old global plugin and immediately relaunch with the bundled runtime plugin.
@@ -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.

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