Skip to content

feat(tokenizer): use Yomitan word classes for subtitle POS filtering#57

Open
ksyasuda wants to merge 35 commits intomainfrom
tokenizer-updates
Open

feat(tokenizer): use Yomitan word classes for subtitle POS filtering#57
ksyasuda wants to merge 35 commits intomainfrom
tokenizer-updates

Conversation

@ksyasuda
Copy link
Copy Markdown
Owner

@ksyasuda ksyasuda commented Apr 26, 2026

Summary

  • Carry matched headword wordClasses from Yomitan termsFind results through YomitanScanToken into MergedToken POS metadata — no vendored Yomitan changes required
  • Map recognized Yomitan word classes (prt, aux, v*, adj-i/ix, adj-na, noun-like) to SubMiner coarse PartOfSpeech/pos1 before annotation filtering
  • MeCab whole-line enrichment now only skips tokens with complete pos1+pos2+pos3; fills missing detail fields while preserving existing coarse pos1 set by Yomitan word class mapping
  • Expand subtitle annotation exclusion rules: standalone grammar particles (か/は/を/…), して helper fragments, single-kana surface fragments, もんか/ものか phrases, bare くれ, って, べき, たって
  • Preserve isKnown status on excluded tokens that have kanji surface/headword, clearing frequency/JLPT/N+1 metadata
  • Pass sourceText through annotation options to support sentence-boundary counting across Yomitan punctuation gaps (groundwork for TASK-304)
  • JLPT level cleared from standalone particle tokens (previously incorrectly retained)

Testing

  • bun test src/core/services/tokenizer/yomitan-parser-runtime.test.ts — new test: requestYomitanScanTokens preserves matched headword word classes
  • bun test src/core/services/tokenizer.test.ts — new tests: Yomitan word class → POS mapping, MeCab detail fill with coarse POS already set, known-word highlight for non-independent kanji nouns; updated particle JLPT test expectation
  • bun test src/core/services/tokenizer/annotation-stage.test.ts — new tests: して/particle/single-kana/もんか/くれ/って/べき exclusion, source-text sentence boundary gap, preserved isKnown for excluded kanji tokens
  • bun run typecheck — passed

Summary by CodeRabbit

  • New Features

    • MPV fullscreen event exposed; Linux fullscreen overlay refresh burst and Hyprland placement controls; AniList post-watch can be forced; Anki integration emits known-word updates; numeric selection accepts modifier chords/numpad; JLPT token styling changed to colored underlines; stats & overlay windows gain improved titles/placement behavior.
  • Bug Fixes

    • Subtitle/tokenization: grammar endings split, kana-only fragments excluded from learning/N+1, annotations (JLPT/frequency/known) cleared/preserved correctly; overlay visibility/cleanup and time-pos/subtitle event ordering fixed; play-next-subtitle pause/unpause behavior improved.
  • Tests

    • Broadly expanded coverage across tokenization, MPV/overlay, Hyprland, keyboard, plugin bindings, Anilist, Anki, stats and lifecycle flows.

Checklist

  • Added a changelog fragment in changes/, or this PR is labeled skip-changelog

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

Tokenizer now preserves Yomitan wordClasses, splits standalone grammar endings, tightens subtitle-annotation exclusions (kana-only, helper spans), coalesces Mecab POS per-field, and improves N+1/sentence detection using sourceText gaps. Separately: MPV fullscreen events, Linux overlay refresh bursts, Hyprland placement, input/keyboard/plugin numeric bindings, Anki known-word notifications, UI/window updates, and many tests.

Changes

Tokenizer / Subtitle Annotation (single DAG)

Layer / File(s) Summary
Scan token shape
src/core/services/tokenizer/yomitan-parser-runtime.ts, src/core/services/tokenizer/yomitan-parser-runtime.test.ts
Add optional wordClasses?: string[]; validate/normalize and emit when non-empty; tests assert preservation and exact-frequency selection when readings differ.
Yomitan → POS mapping
src/core/services/tokenizer.ts
Add helpers to normalize wordClasses and derive { partOfSpeech, pos1? }; TokenizerAnnotationOptions gains sourceText.
Parser selection / splitting
src/core/services/tokenizer/parser-selection-stage.ts, src/core/services/tokenizer/parser-selection-stage.test.ts
Detect standalone grammar-ending segments, flush combined merged unit, and emit grammar-ending as its own merged token; tests updated (e.g., 猫です + です).
Subtitle exclusion predicates
src/core/services/tokenizer/subtitle-annotation-filter.ts
Add predicates for kana-only fragments, auxiliary-inflection/helper spans, してする helpers, standalone grammar particles, single-kana fragments; expand excluded terms and special-case くれくれる.
Annotation stage wiring
src/core/services/tokenizer/annotation-stage.ts, src/core/services/tokenizer/annotation-stage.test.ts
Forward sourceText into N+1 marking; delegate exclusion/stripping to shared filter; when excluded, return stripped token with isKnown:false; many tests extended/tightened.
Enrichment coalescing
src/core/services/tokenizer/parser-enrichment-stage.ts, src/core/services/tokenizer/parser-enrichment-stage.test.ts
Introduce coalesceMissingPosField; require pos1/pos2/pos3 present to skip enrichment; merge MeCab/surface-sequence fields per-field; add backfill test for blank pos2/pos3.
Token merging / N+1 selection
src/token-merger.ts, src/core/services/tokenizer.test.ts
Normalize sourceText for offsets; reject kana-only surface as N+1 candidate; sentence word counts use isKnown/candidate predicate; detect sentence boundaries in source gaps; markNPlusOneTargets accepts sourceText.
Tests / Regression
src/core/services/tokenizer.test.ts, src/core/services/tokenizer/annotation-stage.test.ts, many others
Extensive test updates covering wordClasses-driven POS, JLPT/frequency clearing for excluded tokens, kana-only N+1 exclusions, merged/split scenarios, explanatory endings, known-word edge cases, and hover/annotation behaviors.

MPV fullscreen, Linux overlay refresh & lifecycle (single DAG)

Layer / File(s) Summary
MPV property observation
src/core/services/mpv-properties.ts
Add fullscreen to observed MPV properties.
MPV protocol handling
src/core/services/mpv-protocol.ts, src/core/services/mpv-protocol.test.ts
Handle property-change fullscreen by coercing to boolean and calling new deps.emitFullscreenChange; reorder time-pos handling to update state before emitting; tests added for ordering and fullscreen emit.
IPC client/event wiring
src/core/services/mpv.ts, src/core/services/mpv.test.ts, src/main/runtime/mpv-client-event-bindings.ts
Add 'fullscreen-change' client event and wire emitFullscreenChange through protocol deps into client binder; tests updated.
Main event bindings & deps
src/main/runtime/mpv-main-event-bindings.ts, src/main/runtime/mpv-main-event-main-deps.ts, tests
Propagate optional onFullscreenChange through binder/deps chain; main-deps exposes callback and updates initial-args gating (managedPlayback added); tests updated.
Linux overlay refresh scheduler
src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts, src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
Add cancellable Linux-only burst scheduler (0,50,150,300,600ms) to refresh visible overlay after MPV fullscreen changes: update visibility, hide/show inactive, reapply window level; export clear function and unit test.
App lifecycle integration
src/main.ts, src/main/runtime/app-lifecycle-actions.ts, src/main/runtime/app-lifecycle-main-cleanup.ts, tests
Schedule and cancel Linux refresh burst on fullscreen changes and overlay visibility transitions; add cleanup dep to clear pending timeouts on quit/close; tests updated to include cleanup call.
playNextSubtitle ordering change
src/core/services/mpv.ts, tests
Change playNextSubtitle() to set pendingPauseAtSubEnd, clear pauseAtTime, issue sub-seek, and conditionally unpause when playbackPaused === true; tests updated.

Miscellaneous unrelated DAGs (single cohort)

Layer / File(s) Summary
Renderer mouse/hover
src/renderer/handlers/mouse.ts, src/renderer/handlers/mouse.test.ts
When suppressDirectHoverEnterSource set, consult provided MouseEvent coordinates (via elementFromPoint) before clearing suppression; tests updated to use coordinate-based enter.
Keyboard numeric selection
src/renderer/handlers/keyboard.ts, src/renderer/handlers/keyboard.test.ts
Accept digit from e.key or e.code (Digit*/Numpad*) and remove modifier-key rejection so modifier-chord numeric starters work; test for modifier-based numeric binding added.
Plugin numeric-binding modifier handling
plugin/subminer/session_bindings.lua, scripts/test-plugin-session-bindings.lua, package.json
Build modifier-prefix variants for numeric selection, handle Key[A-Z]+Shift mapping, register forced digit bindings for each prefix; add Lua regression test and include it in plugin test script.
JLPT styling & tests
src/renderer/style.css, src/renderer/subtitle-render.test.ts
Replace JLPT text-decoration underline with permanent border-bottom: 2px solid var(--subtitle-jlpt-n*-color) and add explicit ::selection rules locking text-decoration-color; tests updated to assert no text recolor and selection color-locking.
Hyprland placement & tracker
src/core/services/hyprland-window-placement.ts, src/window-trackers/hyprland-tracker.ts, tests
Add Hyprland placement utilities (parse hyprctl -j monitors/clients, build dispatches to float/pin/move/resize windows, ensure floating by title); tracker uses monitor-aware fullscreen geometry resolution and poll-burst scheduling; tests added.
Anki known-word notifications & card update narrowing
src/anki-integration.ts, src/anki-integration/known-word-cache.ts, src/anki-integration/card-creation.ts, tests
KnownWordCacheManager.appendFromNoteInfo now returns boolean; AnkiIntegration adds setKnownWordCacheUpdatedCallback and notifies on changes; clipboard-driven card update now updates only the sentence-audio field; tests adjusted.
IPC manual mark hook & Anilist post-watch force option
src/core/services/ipc.ts, src/main/runtime/anilist-post-watch.ts, deps/tests
Add optional runAnilistPostWatchUpdateOnManualMark invoked after successful manual mark; post-watch runner gains force?: boolean to bypass watched-duration gating and probe; tests added/updated.
Stats server/source renaming & startup config
src/main/runtime/stats-server-routing.ts, src/main/runtime/startup-config.ts, tests
Rename stats-server source 'foreign''background'; refreshAnilistClientSecretState accepts allowSetupPrompt?: boolean and is called with { force: true, allowSetupPrompt: false } during reload; tests updated.
Overlay/window & stats window options
src/core/services/overlay-window.ts, src/core/services/overlay-window-options.ts, src/core/services/stats-window-runtime.ts, src/core/services/stats-window.ts, tests
Centralize overlay/stats window titles, change non-darwin overlay behavior (use setVisibleOnAllWorkspaces, Hyprland float), change stats window visuals (transparent: false, new background), add outer-bounds resolution and Hyprland enforcement during show; tests updated.
Misc tests & packaging
package.json, many test files
Include new Lua plugin test in test:plugin:src, add stats CSS test to core test list, and update many unit/integration tests to reflect behavior changes across subsystems.

Possibly related PRs

Suggested labels

codex

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch tokenizer-updates

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/core/services/tokenizer/parser-enrichment-stage.ts (1)

306-334: ⚠️ Potential issue | 🟠 Major

Treat blank POS fields as missing during backfill.

The new completeness check treats '' and whitespace-only POS fields as incomplete, but both fill paths still use ??. That means tokens with pos2: '' / pos3: '' will never be populated from MeCab and stay permanently half-enriched.

Suggested fix
+function coalesceMissingPosField(
+  current: string | undefined,
+  fallback: string | undefined,
+): string | undefined {
+  return typeof current === 'string' && current.trim().length > 0 ? current : fallback;
+}
+
 function fillMissingPos1BySurfaceSequence(
   tokens: MergedToken[],
   byTrimmedSurface: Map<string, IndexedMecabToken[]>,
 ): MergedToken[] {
@@
     return {
       ...token,
-      pos1: token.pos1 ?? best.pos1,
-      pos2: token.pos2 ?? best.pos2,
-      pos3: token.pos3 ?? best.pos3,
+      pos1: coalesceMissingPosField(token.pos1, best.pos1),
+      pos2: coalesceMissingPosField(token.pos2, best.pos2),
+      pos3: coalesceMissingPosField(token.pos3, best.pos3),
     };
   });
 }
@@
     return {
       ...token,
-      pos1: token.pos1 ?? metadata.pos1,
-      pos2: token.pos2 ?? metadata.pos2,
-      pos3: token.pos3 ?? metadata.pos3,
+      pos1: coalesceMissingPosField(token.pos1, metadata.pos1),
+      pos2: coalesceMissingPosField(token.pos2, metadata.pos2),
+      pos3: coalesceMissingPosField(token.pos3, metadata.pos3),
     };
   });

Also applies to: 387-417

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/parser-enrichment-stage.ts` around lines 306 -
334, The completeness check now treats whitespace-only POS fields as missing,
but the backfill still uses nullish coalescing (pos1: token.pos1 ?? best.pos1
etc.), so blank strings never get replaced; change each backfill to test for a
non-empty trimmed value (e.g., use token.pos1?.trim() ? token.pos1 : best.pos1)
for pos1, pos2, pos3 so whitespace/empty strings are replaced by MeCab values;
make the same change in the second analogous block (the other backfill around
the lowerBoundByIndex/cursor logic) so both enrichment paths use trimmed
emptiness checks instead of ??.
🧹 Nitpick comments (2)
src/core/services/tokenizer/annotation-stage.test.ts (1)

790-799: Optional: simplify this test fixture to only relevant deps.

isKnownWord / getJlptLevel stubs here are unrelated to the assertion and can be removed (or asserted) to keep the test intent tighter.

✂️ Suggested simplification
-  const result = annotateTokens(
-    tokens,
-    makeDeps({
-      isKnownWord: (text) => text === 'た' || text === '負',
-      getJlptLevel: (text) => (text === 'た' || text === '負' ? 'N3' : null),
-    }),
-    {
-      minSentenceWordsForNPlusOne: 1,
-    },
-  );
+  const result = annotateTokens(tokens, makeDeps(), {
+    minSentenceWordsForNPlusOne: 1,
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/annotation-stage.test.ts` around lines 790 - 799,
The test fixture passes extraneous stubs (isKnownWord/getJlptLevel) into
makeDeps when calling annotateTokens; simplify by removing those unused stubs
and only providing the minimal dependencies required for the assertion (or
replace them with noop/default implementations), keeping the annotateTokens call
and the minSentenceWordsForNPlusOne option intact; if you want to ensure they
aren't used, add an explicit assertion/mock that verifies
isKnownWord/getJlptLevel are not invoked rather than stubbing meaningful
behavior.
src/core/services/tokenizer/subtitle-annotation-filter.ts (1)

23-26: Optional: deduplicate shared standalone phrase literals.

たって / だって are maintained in multiple constants. Consider deriving both consumers from one source list to reduce drift risk.

♻️ Suggested refactor
+const STANDALONE_GRAMMAR_PARTICLE_PHRASES = new Set(['たって', 'だって']);
+
 const SUBTITLE_ANNOTATION_EXCLUDED_TERMS = new Set([
   'あ',
   'ああ',
   'ええ',
   'うう',
   'おお',
   'くれ',
-  'たって',
   'って',
-  'だって',
+  ...STANDALONE_GRAMMAR_PARTICLE_PHRASES,
   'はあ',
   'はは',
   'べき',
   'へえ',
   'ふう',
   'ほう',
   'もんか',
   'ものか',
 ]);
-const STANDALONE_GRAMMAR_PARTICLE_PHRASES = new Set(['たって', 'だって']);

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/subtitle-annotation-filter.ts` around lines 23 -
26, Multiple literal standalone phrases like 'たって' and 'だって' are duplicated
across constants in subtitle-annotation-filter.ts; create a single shared
constant (e.g., SHARED_STANDALONE_PHRASES) containing those common entries and
replace the duplicated arrays referenced by the two consumers (the
constants/arrays that currently list 'たって'/'だって') so both derive from that one
source to avoid drift and ensure future updates stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/tokenizer.ts`:
- Around line 784-789: The code currently returns tokens produced by
parseWithYomitanInternalParser without calling stripSubtitleAnnotationMetadata
when applyAnnotationStage returns early, which lets subtitle-excluded tokens
keep annotation fields; update the no-annotation return path so that before
returning (after const yomitanTokens / when annotatedTokens may be skipped) you
call stripSubtitleAnnotationMetadata(yomitanTokens) and use that filtered array
in the returned object; ensure stripSubtitleAnnotationMetadata is invoked in the
branch that currently returns early from applyAnnotationStage and that the
returned object still uses annotationOptions.sourceText and other existing
fields unchanged.

---

Outside diff comments:
In `@src/core/services/tokenizer/parser-enrichment-stage.ts`:
- Around line 306-334: The completeness check now treats whitespace-only POS
fields as missing, but the backfill still uses nullish coalescing (pos1:
token.pos1 ?? best.pos1 etc.), so blank strings never get replaced; change each
backfill to test for a non-empty trimmed value (e.g., use token.pos1?.trim() ?
token.pos1 : best.pos1) for pos1, pos2, pos3 so whitespace/empty strings are
replaced by MeCab values; make the same change in the second analogous block
(the other backfill around the lowerBoundByIndex/cursor logic) so both
enrichment paths use trimmed emptiness checks instead of ??.

---

Nitpick comments:
In `@src/core/services/tokenizer/annotation-stage.test.ts`:
- Around line 790-799: The test fixture passes extraneous stubs
(isKnownWord/getJlptLevel) into makeDeps when calling annotateTokens; simplify
by removing those unused stubs and only providing the minimal dependencies
required for the assertion (or replace them with noop/default implementations),
keeping the annotateTokens call and the minSentenceWordsForNPlusOne option
intact; if you want to ensure they aren't used, add an explicit assertion/mock
that verifies isKnownWord/getJlptLevel are not invoked rather than stubbing
meaningful behavior.

In `@src/core/services/tokenizer/subtitle-annotation-filter.ts`:
- Around line 23-26: Multiple literal standalone phrases like 'たって' and 'だって'
are duplicated across constants in subtitle-annotation-filter.ts; create a
single shared constant (e.g., SHARED_STANDALONE_PHRASES) containing those common
entries and replace the duplicated arrays referenced by the two consumers (the
constants/arrays that currently list 'たって'/'だって') so both derive from that one
source to avoid drift and ensure future updates stay in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 26d4b00d-d9bd-41b3-8054-dc6976da93e5

📥 Commits

Reviewing files that changed from the base of the PR and between d893464 and 6b7d055.

⛔ Files ignored due to path filters (2)
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
📒 Files selected for processing (9)
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/token-merger.ts

Comment thread src/core/services/tokenizer.ts
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 27, 2026

Deploying subminer-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4d5bf3d
Status: ✅  Deploy successful!
Preview URL: https://0edfeb54.subminer.pages.dev
Branch Preview URL: https://tokenizer-updates.subminer.pages.dev

View logs

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/core/services/tokenizer/annotation-stage.test.ts (1)

1073-1320: Recommend splitting this test file by concern.

Grouping exclusion predicates, metadata stripping, and N+1 behavior into separate suites/files would keep this TS module easier to evolve.

As per coding guidelines, "TypeScript files should be kept small and follow existing patterns".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/annotation-stage.test.ts` around lines 1073 -
1320, The test file currently mixes three concerns around annotateTokens
(exclusion predicates, metadata stripping, and N+1 behavior) which makes it
large and hard to maintain; split the suite into separate test modules (e.g.,
annotateTokens.exclusions.test.ts, annotateTokens.metadata.test.ts,
annotateTokens.nplusone.test.ts) by moving the relevant tests that reference
annotateTokens and makeDeps (the tests for standalone helpers/particles/phrases
that clear annotations, the tests asserting frequencyRank/jlptLevel cleared, and
the tests asserting isNPlusOneTarget behavior) into their respective files,
retain the same test names and assertions, update imports for annotateTokens,
makeDeps, makeToken, and PartOfSpeech in each new file, and run/update the test
runner configuration if needed so the new files are discovered.
src/core/services/tokenizer.test.ts (1)

3062-3249: Consider splitting this test file into focused suites.

This file now mixes parser selection, enrichment, exclusion, and N+1 behavior in one very large TS module. Splitting by concern would improve failure triage and long-term maintainability.

As per coding guidelines, "TypeScript files should be kept small and follow existing patterns".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer.test.ts` around lines 3062 - 3249, The test file
mixes multiple concerns; split tests into focused suites by creating separate
test modules for parser selection (tests referencing tokenizeWithMecab
behavior), enrichment (tests asserting frequencyRank/jlptLevel like
"tokenizeSubtitle keeps frequency enrichment..."), exclusion/non-independent
pos2 behavior (the "excludes default non-independent pos2..." test), and
N+1/known-word behavior (tests using isKnownWord/getNPlusOneEnabled), moving the
corresponding test cases that call tokenizeSubtitle and use
makeDepsFromYomitanTokens into those files, export or import shared helpers
(makeDepsFromYomitanTokens, PartOfSpeech) so each new file can reuse them, keep
test names and assertions intact, and update any test runner imports/exports so
all new test files are discovered by the test suite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.ts`:
- Around line 1944-1958: The refreshLinuxVisibleOverlayAfterMpvFullscreenChange
function currently calls mainWindow.show() after hide(), which will steal focus
on Linux because the overlay is focusable; replace that call with
mainWindow.showInactive() so the overlay becomes visible without activating
input focus. Update the code in
refreshLinuxVisibleOverlayAfterMpvFullscreenChange (where it obtains mainWindow
via overlayManager.getMainWindow and calls ensureOverlayWindowLevel) to use
showInactive() instead of show(), keeping the preceding hide() and
ensureOverlayWindowLevel calls unchanged.

In `@src/token-merger.ts`:
- Around line 373-384: The gap-check is currently slicing the unnormalized
sourceText which can desynchronize offsets computed from the normalized text
(see markNPlusOneTargets); update hasSentenceBoundaryInSourceGap to normalize
sourceText with the same normalization routine used when computing token offsets
(the same function used around line 181) before creating gap =
sourceText.slice(previousEnd, nextStart), and apply the same
normalization-before-slicing change to the other gap-check sites referenced
(around lines 391 and 434-437) so all sentence-boundary checks operate on the
normalized text that matches the token offset space.

In `@src/window-trackers/hyprland-tracker.ts`:
- Around line 351-357: The getHyprlandMonitors function can throw when running
execSync('hyprctl -j monitors') or when parseHyprctlMonitors fails, causing
caller resolveHyprlandWindowGeometry to be unable to fall back to
client.at/client.size; wrap the execSync + parseHyprctlMonitors call in a
try/catch and return null on any error or parse failure so the caller can
degrade gracefully, referencing getHyprlandMonitors, execSync('hyprctl -j
monitors'), parseHyprctlMonitors, and
resolveHyprlandWindowGeometry/client.at/client.size.

---

Nitpick comments:
In `@src/core/services/tokenizer.test.ts`:
- Around line 3062-3249: The test file mixes multiple concerns; split tests into
focused suites by creating separate test modules for parser selection (tests
referencing tokenizeWithMecab behavior), enrichment (tests asserting
frequencyRank/jlptLevel like "tokenizeSubtitle keeps frequency enrichment..."),
exclusion/non-independent pos2 behavior (the "excludes default non-independent
pos2..." test), and N+1/known-word behavior (tests using
isKnownWord/getNPlusOneEnabled), moving the corresponding test cases that call
tokenizeSubtitle and use makeDepsFromYomitanTokens into those files, export or
import shared helpers (makeDepsFromYomitanTokens, PartOfSpeech) so each new file
can reuse them, keep test names and assertions intact, and update any test
runner imports/exports so all new test files are discovered by the test suite.

In `@src/core/services/tokenizer/annotation-stage.test.ts`:
- Around line 1073-1320: The test file currently mixes three concerns around
annotateTokens (exclusion predicates, metadata stripping, and N+1 behavior)
which makes it large and hard to maintain; split the suite into separate test
modules (e.g., annotateTokens.exclusions.test.ts,
annotateTokens.metadata.test.ts, annotateTokens.nplusone.test.ts) by moving the
relevant tests that reference annotateTokens and makeDeps (the tests for
standalone helpers/particles/phrases that clear annotations, the tests asserting
frequencyRank/jlptLevel cleared, and the tests asserting isNPlusOneTarget
behavior) into their respective files, retain the same test names and
assertions, update imports for annotateTokens, makeDeps, makeToken, and
PartOfSpeech in each new file, and run/update the test runner configuration if
needed so the new files are discovered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 543913f2-92af-471b-991e-a5da46d21d75

📥 Commits

Reviewing files that changed from the base of the PR and between dc9d134 and bfa9b10.

⛔ Files ignored due to path filters (7)
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
📒 Files selected for processing (21)
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/main.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (2)
  • src/core/services/mpv-properties.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts

Comment thread src/main.ts Outdated
Comment thread src/token-merger.ts
Comment thread src/window-trackers/hyprland-tracker.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/window-trackers/hyprland-tracker.ts (1)

304-313: Coalesce burst polling without a synchronous immediate call.

scheduleGeometryPollBurst() calls pollGeometry() inline, and pollGeometry() uses execSync. On noisy geometry events (windowtitle*, workspace*), this can block the event loop repeatedly. Consider scheduling 0ms instead of immediate execution so events are coalesced first.

Suggested diff
   private scheduleGeometryPollBurst(): void {
-    this.pollGeometry();
     for (const timeout of this.pollTimeouts) {
       clearTimeout(timeout);
     }
-    this.pollTimeouts = [50, 150, 300].map((delayMs) => {
+    this.pollTimeouts = [0, 50, 150, 300].map((delayMs) => {
       const pollTimeout = setTimeout(() => {
         this.pollTimeouts = this.pollTimeouts.filter((timeout) => timeout !== pollTimeout);
         this.pollGeometry();
       }, delayMs);
       return pollTimeout;

Also applies to: 321-324

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/window-trackers/hyprland-tracker.ts` around lines 304 - 313,
scheduleGeometryPollBurst currently calls pollGeometry synchronously (which uses
execSync) and can block the event loop on noisy events; change it to schedule an
immediate (setTimeout(..., 0)) call instead of invoking pollGeometry inline and
similarly replace any other direct pollGeometry calls in this function (and the
related block at 321-324) so the initial poll is queued and all bursts are
coalesced via pollTimeouts; update scheduleGeometryPollBurst to push a 0ms
timeout that calls pollGeometry and removes itself from this.pollTimeouts, and
ensure pollTimeouts is still cleared at the start of the method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/window-trackers/hyprland-tracker.ts`:
- Around line 304-313: scheduleGeometryPollBurst currently calls pollGeometry
synchronously (which uses execSync) and can block the event loop on noisy
events; change it to schedule an immediate (setTimeout(..., 0)) call instead of
invoking pollGeometry inline and similarly replace any other direct pollGeometry
calls in this function (and the related block at 321-324) so the initial poll is
queued and all bursts are coalesced via pollTimeouts; update
scheduleGeometryPollBurst to push a 0ms timeout that calls pollGeometry and
removes itself from this.pollTimeouts, and ensure pollTimeouts is still cleared
at the start of the method.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 879ad861-b578-4566-98ff-6f7f41b0c593

📥 Commits

Reviewing files that changed from the base of the PR and between bfa9b10 and 7749c60.

📒 Files selected for processing (3)
  • src/main.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/token-merger.ts

@ksyasuda ksyasuda force-pushed the tokenizer-updates branch from 7749c60 to c150fce Compare April 27, 2026 08:34
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main.ts (1)

1914-1978: Extract the Linux fullscreen refresh helper out of src/main.ts.

This logic is reasonable, but adding another platform-specific timer/burst helper to an already massive file makes src/main.ts harder to maintain. Moving the Linux fullscreen refresh constants and helpers into a small runtime module would better match the rest of the runtime composition pattern.

As per coding guidelines, TypeScript files should be kept small and follow existing patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 1914 - 1978, Extract the Linux fullscreen refresh
logic (const LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS, state var
linuxMpvFullscreenOverlayRefreshTimeouts, and the functions
clearLinuxMpvFullscreenOverlayRefreshTimeouts,
refreshLinuxVisibleOverlayAfterMpvFullscreenChange,
scheduleLinuxVisibleOverlayFullscreenRefreshBurst) into a new small runtime
module; move the constants/vars and functions out of src/main.ts, export the
functions, and update src/main.ts to import and call the exported schedule/clear
helpers. In the new module import any runtime dependencies used inside those
functions (overlayManager, overlayVisibilityRuntime, ensureOverlayWindowLevel)
from their existing modules, preserve the timeout unref() behavior and the same
filtering logic when clearing timeouts, and ensure types (ReturnType<typeof
setTimeout>) are kept so the new file compiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/tokenizer.ts`:
- Around line 355-364: The current check in
resolvePartOfSpeechFromYomitanWordClasses only matches the exact 'aux' token so
subclasses like 'aux-v' or 'aux-adj' fall through; update the auxiliary
detection to mirror the verb/noun pattern (e.g., use a startsWith/substring test
or any match that treats tokens beginning with 'aux' as auxiliary) and still
return { partOfSpeech: PartOfSpeech.bound_auxiliary, pos1: '助動詞' } so auxiliary
subclasses are correctly classified and retain subtitle exclusion and
JLPT/frequency suppression behavior.

---

Nitpick comments:
In `@src/main.ts`:
- Around line 1914-1978: Extract the Linux fullscreen refresh logic (const
LINUX_MPV_FULLSCREEN_OVERLAY_REFRESH_DELAYS_MS, state var
linuxMpvFullscreenOverlayRefreshTimeouts, and the functions
clearLinuxMpvFullscreenOverlayRefreshTimeouts,
refreshLinuxVisibleOverlayAfterMpvFullscreenChange,
scheduleLinuxVisibleOverlayFullscreenRefreshBurst) into a new small runtime
module; move the constants/vars and functions out of src/main.ts, export the
functions, and update src/main.ts to import and call the exported schedule/clear
helpers. In the new module import any runtime dependencies used inside those
functions (overlayManager, overlayVisibilityRuntime, ensureOverlayWindowLevel)
from their existing modules, preserve the timeout unref() behavior and the same
filtering logic when clearing timeouts, and ensure types (ReturnType<typeof
setTimeout>) are kept so the new file compiles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c7c11784-5070-4f43-b3f3-50f40f2db837

📥 Commits

Reviewing files that changed from the base of the PR and between 7749c60 and c150fce.

⛔ Files ignored due to path filters (10)
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • changes/305-tokenizer-word-class-pos-filtering.md is excluded by !changes/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
📒 Files selected for processing (28)
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/main.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (7)
  • src/core/services/mpv.test.ts
  • src/core/services/overlay-window.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/style.css
  • src/window-trackers/hyprland-tracker.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/renderer/handlers/mouse.test.ts
  • src/core/services/mpv-protocol.ts
  • src/renderer/handlers/mouse.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/token-merger.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/window-trackers/hyprland-tracker.test.ts

Comment thread src/core/services/tokenizer.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/core/services/tokenizer.test.ts (1)

3062-3790: Please split these new tokenizer scenarios into smaller test files.

This file is already very large; continuing to add dense blocks here makes navigation and maintenance harder. Consider extracting these additions into focused suites (for example: tokenizer.word-classes.test.ts and tokenizer.subtitle-exclusions.test.ts).

As per coding guidelines, TypeScript files should be kept small and follow existing patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer.test.ts` around lines 3062 - 3790, The tokenizer
test additions (tests invoking tokenizeSubtitle with scenarios like "uses
Yomitan word classes..." "subtitle-exclusions", e.g. tests referencing
PartOfSpeech and helper makeDepsFromYomitanTokens/makeDeps) should be split into
smaller focused test files; create new suites such as
tokenizer.word-classes.test.ts (move tests about Yomitan wordClasses, auxiliary
subclasses, detailed MeCab POS) and tokenizer.subtitle-exclusions.test.ts (move
tests about frequency/N+1 exclusions, interjections, merged tokens, hoverable
grammar tails), copy the relevant tests (preserving describe/test names and any
helper usages of makeDeps/makeDepsFromYomitanTokens and PartOfSpeech), update
any shared helper imports so each new file imports the same helpers currently
used in tokenizer.test.ts, and remove the moved tests from the original file to
keep it small and consistent with existing test patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.ts`:
- Around line 3815-3824: The scheduled Linux fullscreen-refresh burst created
via scheduleLinuxVisibleOverlayFullscreenRefreshBurst (called from the
onFullscreenChange closure) can outlive the visible overlay and act on a
recreated window; fix by capturing the cancellation handle returned from
scheduleLinuxVisibleOverlayFullscreenRefreshBurst when calling it (from
onFullscreenChange) and invoke that cancel function whenever the visible overlay
is hidden or closed (e.g., in the overlay visibility hide/close handlers that
call into overlayManager / overlayVisibilityRuntime and any recreate path),
ensuring you also keep the existing quit-time clear; update the visible-overlay
hide/close code paths to call the stored cancel before destroying or replacing
the window so pending callbacks don’t run against the new window.

In `@src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts`:
- Around line 35-40: Replace the fixed 10ms sleep with a deterministic wait:
instead of awaiting new Promise(setTimeout(..., 10)), poll or await until the
expected callback has been invoked (e.g., wait in a loop with short yields until
calls.includes('updateVisibleOverlayVisibility') becomes true) or use controlled
timers (sinon/lolex fake timers) and advance them so the first setTimeout(…, 0)
runs; ensure the assertions then run after that awaited condition so the checks
for updateVisibleOverlayVisibility, hide, showInactive, and
ensureOverlayWindowLevel are stable.

---

Nitpick comments:
In `@src/core/services/tokenizer.test.ts`:
- Around line 3062-3790: The tokenizer test additions (tests invoking
tokenizeSubtitle with scenarios like "uses Yomitan word classes..."
"subtitle-exclusions", e.g. tests referencing PartOfSpeech and helper
makeDepsFromYomitanTokens/makeDeps) should be split into smaller focused test
files; create new suites such as tokenizer.word-classes.test.ts (move tests
about Yomitan wordClasses, auxiliary subclasses, detailed MeCab POS) and
tokenizer.subtitle-exclusions.test.ts (move tests about frequency/N+1
exclusions, interjections, merged tokens, hoverable grammar tails), copy the
relevant tests (preserving describe/test names and any helper usages of
makeDeps/makeDepsFromYomitanTokens and PartOfSpeech), update any shared helper
imports so each new file imports the same helpers currently used in
tokenizer.test.ts, and remove the moved tests from the original file to keep it
small and consistent with existing test patterns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 590b81a7-2e46-4e08-a72f-21858ee25599

📥 Commits

Reviewing files that changed from the base of the PR and between c150fce and bacc90c.

⛔ Files ignored due to path filters (2)
  • backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md is excluded by !backlog/**
  • changes/309-multi-mine-modified-digits.md is excluded by !changes/**
📒 Files selected for processing (18)
  • package.json
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/main.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (2)
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.ts
  • src/core/services/tokenizer/annotation-stage.test.ts

Comment thread src/main.ts
Comment thread src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/core/services/tokenizer/annotation-stage.test.ts (1)

369-493: Collapse these exclusion cases into table-driven assertions.

The coverage is solid, but both blocks repeat the same single-token setup and “all annotation fields cleared” assertions many times. A small helper plus subtests/data tables would keep this already-large file much easier to extend when more exclusions are added.

As per coding guidelines, "TypeScript files should be kept small and follow existing patterns".

Also applies to: 1207-1418

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/annotation-stage.test.ts` around lines 369 - 493,
The tests repeatedly construct single-token cases to assert
shouldExcludeTokenFromSubtitleAnnotations returns true; refactor by replacing
the repeated test blocks with a single table-driven test: create a data array of
token-parameter objects (using makeToken and PartOfSpeech values) and iterate
(or use test.each) to run a subtest per entry, asserting
shouldExcludeTokenFromSubtitleAnnotations(token) === true and including
token.surface as the subtest name; consolidate similar groups (e.g., standalone
particles, grammar helpers, single-kana) into grouped tables to keep the file
compact and follow existing patterns.
src/token-merger.ts (1)

286-327: Reuse the existing kana-normalization path for N+1 eligibility.

subtitle-annotation-filter.ts already defines similar kana-only exclusion behavior, but this new helper stack reimplements it on raw code points. That gives N+1 targeting a second “kana-only” definition and makes drift likely if the normalization rules change in one place but not the other. Please route this through the shared tokenizer helper, or normalize with the same path before checking here.

As per coding guidelines, "TypeScript files should be kept small and follow existing patterns".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/token-merger.ts` around lines 286 - 327, Replace the ad-hoc kana
detection in isKanaChar/isKanaCandidateIgnorableChar/isKanaOnlyText with the
shared tokenizer/normalization used by subtitle-annotation-filter.ts: import the
canonical normalization/tokenizer helper (the function used there to produce the
kana-normalized text) and call it from normalizeSourceTextForTokenOffsets or at
the top of isKanaOnlyText, then apply the existing kana-only check against that
normalized output instead of raw code points; update isKanaOnlyText to return
false on empty normalized result and keep the existing hasKana logic but
operating on the shared-normalized string so both modules use the same
normalization path and avoid drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/services/tokenizer/annotation-stage.test.ts`:
- Around line 369-493: The tests repeatedly construct single-token cases to
assert shouldExcludeTokenFromSubtitleAnnotations returns true; refactor by
replacing the repeated test blocks with a single table-driven test: create a
data array of token-parameter objects (using makeToken and PartOfSpeech values)
and iterate (or use test.each) to run a subtest per entry, asserting
shouldExcludeTokenFromSubtitleAnnotations(token) === true and including
token.surface as the subtest name; consolidate similar groups (e.g., standalone
particles, grammar helpers, single-kana) into grouped tables to keep the file
compact and follow existing patterns.

In `@src/token-merger.ts`:
- Around line 286-327: Replace the ad-hoc kana detection in
isKanaChar/isKanaCandidateIgnorableChar/isKanaOnlyText with the shared
tokenizer/normalization used by subtitle-annotation-filter.ts: import the
canonical normalization/tokenizer helper (the function used there to produce the
kana-normalized text) and call it from normalizeSourceTextForTokenOffsets or at
the top of isKanaOnlyText, then apply the existing kana-only check against that
normalized output instead of raw code points; update isKanaOnlyText to return
false on empty normalized result and keep the existing hasKana logic but
operating on the shared-normalized string so both modules use the same
normalization path and avoid drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 09925e0f-1aee-477d-87d8-47e87150e0cc

📥 Commits

Reviewing files that changed from the base of the PR and between bacc90c and 7442e42.

⛔ Files ignored due to path filters (1)
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
📒 Files selected for processing (5)
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/main.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/token-merger.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/tokenizer/annotation-stage.test.ts`:
- Around line 672-709: The test uses a mixed-script surface "スイッチ…" (contains
punctuation) so it won't catch regressions in isKanaOnlyText; update the third
token created by makeToken in the test named "annotateTokens does not mark
kana-only unknown target with subtitle punctuation as N+1" to be truly kana-only
(e.g., change surface from "スイッチ…" to a pure-kana string like "スイッチ" or a
hiragana variant "すいっち" and ensure headword/reading match), so annotateTokens
and the isKanaOnlyText guard are exercised as intended.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d82198ce-4984-4044-86ea-04153e9ee2cf

📥 Commits

Reviewing files that changed from the base of the PR and between bacc90c and 251b6cd.

⛔ Files ignored due to path filters (5)
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
  • backlog/tasks/task-311 - Suppress-auxiliary-inflection-fragments-from-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-312 - Suppress-ja-nai-explanatory-ending-subtitle-annotations.md is excluded by !backlog/**
  • changes/311-auxiliary-inflection-annotation-filter.md is excluded by !changes/**
  • changes/312-grammar-ending-annotation-filter.md is excluded by !changes/**
📒 Files selected for processing (7)
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/main.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/token-merger.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/token-merger.ts

Comment thread src/core/services/tokenizer/annotation-stage.test.ts Outdated
@ksyasuda ksyasuda force-pushed the tokenizer-updates branch from 251b6cd to c7996e3 Compare May 2, 2026 22:44
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/main/runtime/app-lifecycle-actions.test.ts (1)

47-52: ⚡ Quick win

Strengthen this test with order assertions for the new cleanup step.

includes(...) can pass even if the timeout cleanup runs too late. Consider asserting it occurs before window-destroy calls to lock in intended shutdown safety.

Suggested assertion additions
   assert.ok(calls.includes('clear-windows-visible-overlay-poll'));
   assert.ok(calls.includes('clear-linux-mpv-fullscreen-overlay-refresh-timeouts'));
+  assert.ok(
+    calls.indexOf('clear-linux-mpv-fullscreen-overlay-refresh-timeouts') <
+      calls.indexOf('destroy-main-overlay-window'),
+  );
   assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket'));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/runtime/app-lifecycle-actions.test.ts` around lines 47 - 52, The
test currently only checks presence of the new timeout-cleanup steps; strengthen
it by asserting their order relative to window-destroy steps: use the existing
calls array and add index comparisons such as
assert.ok(calls.indexOf('clear-windows-visible-overlay-poll') <
calls.indexOf('destroy-tray')) and
assert.ok(calls.indexOf('clear-linux-mpv-fullscreen-overlay-refresh-timeouts') <
calls.indexOf('destroy-tray')) (or compare against whichever window-destroy
symbol is used in the test) so both cleanup actions run before any
destroy-window/destroy-tray teardown steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/runtime/app-lifecycle-actions.test.ts`:
- Around line 47-52: The test currently only checks presence of the new
timeout-cleanup steps; strengthen it by asserting their order relative to
window-destroy steps: use the existing calls array and add index comparisons
such as assert.ok(calls.indexOf('clear-windows-visible-overlay-poll') <
calls.indexOf('destroy-tray')) and
assert.ok(calls.indexOf('clear-linux-mpv-fullscreen-overlay-refresh-timeouts') <
calls.indexOf('destroy-tray')) (or compare against whichever window-destroy
symbol is used in the test) so both cleanup actions run before any
destroy-window/destroy-tray teardown steps.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: afd75b99-5954-49d0-891f-d7e25e4ea7e7

📥 Commits

Reviewing files that changed from the base of the PR and between 251b6cd and c7996e3.

⛔ Files ignored due to path filters (17)
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md is excluded by !backlog/**
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
  • backlog/tasks/task-311 - Suppress-auxiliary-inflection-fragments-from-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-312 - Suppress-ja-nai-explanatory-ending-subtitle-annotations.md is excluded by !backlog/**
  • changes/305-tokenizer-word-class-pos-filtering.md is excluded by !changes/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • changes/309-multi-mine-modified-digits.md is excluded by !changes/**
  • changes/311-auxiliary-inflection-annotation-filter.md is excluded by !changes/**
  • changes/312-grammar-ending-annotation-filter.md is excluded by !changes/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
📒 Files selected for processing (40)
  • package.json
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/main.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (14)
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/mpv-properties.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/core/services/mpv.ts
  • src/core/services/tokenizer.ts
  • src/main.ts
  • src/token-merger.ts
  • src/core/services/tokenizer.test.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • src/window-trackers/hyprland-tracker.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • package.json
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/renderer/style.css
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/overlay-window.ts
  • scripts/test-plugin-session-bindings.lua
  • src/renderer/handlers/mouse.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/renderer/handlers/keyboard.ts
  • src/window-trackers/hyprland-tracker.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/annotation-stage.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/core/services/tokenizer/parser-selection-stage.ts (1)

27-43: ⚡ Quick win

Centralize grammar-ending lexicon to prevent split/filter drift.

STANDALONE_GRAMMAR_ENDINGS is now a second hardcoded grammar list parallel to src/core/services/tokenizer/subtitle-annotation-filter.ts rules. Divergence here will cause parser-splitting and annotation-filter behavior to silently desync. Consider moving these shared endings into one tokenizer constants module consumed by both stages.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/parser-selection-stage.ts` around lines 27 - 43,
STANDALONE_GRAMMAR_ENDINGS duplicates a grammar lexicon from
subtitle-annotation-filter.ts causing potential drift; extract this set into a
shared tokenizer constants module (e.g., export const STANDALONE_GRAMMAR_ENDINGS
from a new or existing tokenizer/constants.ts), replace the local declaration in
parser-selection-stage.ts with an import of that exported constant, and update
subtitle-annotation-filter.ts to import the same constant so both stages consume
the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/services/tokenizer/parser-selection-stage.ts`:
- Around line 27-43: STANDALONE_GRAMMAR_ENDINGS duplicates a grammar lexicon
from subtitle-annotation-filter.ts causing potential drift; extract this set
into a shared tokenizer constants module (e.g., export const
STANDALONE_GRAMMAR_ENDINGS from a new or existing tokenizer/constants.ts),
replace the local declaration in parser-selection-stage.ts with an import of
that exported constant, and update subtitle-annotation-filter.ts to import the
same constant so both stages consume the single source of truth.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 536dd360-9d89-4c99-8d6b-7d811df19d13

📥 Commits

Reviewing files that changed from the base of the PR and between 756b368 and 03c840c.

⛔ Files ignored due to path filters (3)
  • backlog/tasks/task-315 - Suppress-annotations-for-standalone-じゃない-and-です-ending-tokens.md is excluded by !backlog/**
  • backlog/tasks/task-316 - Fix-macOS-launcher-playback-exit-with-background-stats-daemon.md is excluded by !backlog/**
  • changes/316-macos-playback-stats-daemon.md is excluded by !changes/**
📒 Files selected for processing (9)
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/stats-server-routing.ts
✅ Files skipped from review due to trivial changes (1)
  • src/core/services/tokenizer/annotation-stage.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/services/tokenizer/subtitle-annotation-filter.ts

@ksyasuda ksyasuda force-pushed the tokenizer-updates branch from 03c840c to 738ae17 Compare May 3, 2026 02:53
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
scripts/test-plugin-session-bindings.lua (1)

128-132: ⚡ Quick win

Assert exact CLI argv length to catch silent argument drift.

You already validate values at indices 1..3; adding an argv length check makes this regression stricter.

Proposed fix
 local call = recorded.async_calls[`#recorded.async_calls`]
 assert_true(call ~= nil, "modified digit should invoke CLI action")
+assert_true(`#call` == 3, "CLI action should pass exactly 3 argv entries")
 assert_true(call[1] == "/tmp/subminer", "CLI action should use configured binary")
 assert_true(call[2] == "--mine-sentence-count", "CLI action should mine sentence count")
 assert_true(call[3] == "3", "CLI action should pass selected count")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-plugin-session-bindings.lua` around lines 128 - 132, Add a
strict argv length check after grabbing call from recorded.async_calls: assert
that `#call` equals 3 so silent argument drift is detected; update the test near
the existing assertions that reference call and recorded.async_calls (i.e.,
after local call = recorded.async_calls[`#recorded.async_calls`]) to include an
assertion that the argument vector length is exactly 3 with an explanatory
message.
src/core/services/tokenizer.test.ts (2)

3078-3155: ⚡ Quick win

Broaden the wordClasses mapping matrix a bit.

These cases only exercise prt and aux-v, while the runtime mapping in this PR also covers verb, adjective, and noun-like classes. Adding one representative case for each remaining family would close the main regression gap with very little extra setup.

Possible table-driven shape
+for (const {
+  wordClass,
+  expectedPartOfSpeech,
+  expectedPos1,
+} of [
+  { wordClass: 'v1', expectedPartOfSpeech: PartOfSpeech.verb, expectedPos1: '動詞' },
+  { wordClass: 'adj-i', expectedPartOfSpeech: PartOfSpeech.i_adjective, expectedPos1: '形容詞' },
+  { wordClass: 'n', expectedPartOfSpeech: PartOfSpeech.noun, expectedPos1: '名詞' },
+]) {
+  test(`tokenizeSubtitle maps ${wordClass}`, async () => {
+    // existing makeDepsFromYomitanTokens setup...
+  });
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer.test.ts` around lines 3078 - 3155, Tests for
tokenizeSubtitle only cover 'prt' and 'aux-v' Yomitan wordClasses but the
runtime mapping also handles verb, adjective, and noun-like classes; add
additional table-driven test cases using makeDepsFromYomitanTokens to include
one representative token each with wordClasses like 'v' (or 'vb'), 'adj' (or
'adj-i'), and a noun family (e.g., 'n'/'noun') and assert tokenizeSubtitle
returns the expected PartOfSpeech (PartOfSpeech.verb, PartOfSpeech.adjective,
PartOfSpeech.noun or appropriate constants), plus correct pos1/pos2 when
available; place these alongside the existing tests referencing tokenizeSubtitle
and PartOfSpeech so the mapping coverage is exercised without changing runtime
code.

4246-4449: 🏗️ Heavy lift

Consider moving these filter-centric cases into a focused test file.

This batch is validating subtitle-annotation filtering more than top-level tokenization, and src/core/services/tokenizer.test.ts is already getting hard to scan. Moving these scenarios into src/core/services/tokenizer/annotation-stage.test.ts or a dedicated filter test would keep the integration suite tighter.

As per coding guidelines, "TypeScript files should be kept small and follow existing patterns."

Also applies to: 4670-4914

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer.test.ts` around lines 4246 - 4449, These two
filter-focused tests (the ones named "tokenizeSubtitle clears annotations for
ja-nai explanatory endings while preserving lexical content" and
"tokenizeSubtitle clears annotations for standalone polite copula endings
without POS metadata") should be moved out of the large tokenizer.test.ts and
into a new focused test file named annotation-stage.test.ts; copy the tests
verbatim but update their imports to reference the same helpers
(tokenizeSubtitle, makeDepsFromYomitanTokens, PartOfSpeech) from the module
exports used in the original file, ensure any test helpers remain
exported/imported so the tests compile, and run the suite to confirm the new
test file is detected by the runner.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/test-plugin-session-bindings.lua`:
- Line 13: Replace the hardcoded os.execute("mkdir -p .tmp") call with a
shell-agnostic creation: detect the platform via package.config:sub(1,1) and
call os.execute("mkdir .tmp") on Windows vs os.execute("mkdir -p .tmp") on Unix,
or alternatively use a Lua filesystem API (e.g., lfs.mkdir) if available; update
the os.execute invocation accordingly so the test creates the .tmp directory on
both Windows and Unix runners.

In `@src/core/services/tokenizer.ts`:
- Around line 99-100: The early-return branches call
stripSubtitleAnnotationMetadata(tokens) without forwarding the
TokenizerAnnotationOptions (including the new sourceText), so
sentence-boundary-aware filtering never sees sourceText; update each call site
(the early-return path and the other occurrences around the tokenization logic)
to forward the same TokenizerAnnotationOptions object (or at minimum {
sourceText }) into stripSubtitleAnnotationMetadata(tokens, options), and if
necessary update stripSubtitleAnnotationMetadata's signature to accept and use
TokenizerAnnotationOptions so sourceText is honored during metadata stripping.

In `@src/core/services/tokenizer/parser-selection-stage.ts`:
- Around line 27-43: STANDALONE_GRAMMAR_ENDINGS only covers the "です"/"じゃない"
family but must match the broader copula/explanatory forms used by
subtitle-annotation-filter; update the STANDALONE_GRAMMAR_ENDINGS constant to
include other copula/explanatory endings (e.g., だ, だった, では, ではない, でしょう, だろう, だよ,
だね, なんだ, etc.) so splitting behavior mirrors the subtitle exclusion rules, and
make the same extended change to the duplicate list found around the 162-168
range to keep both definitions consistent (use the existing constant name
STANDALONE_GRAMMAR_ENDINGS to locate and update both occurrences).

---

Nitpick comments:
In `@scripts/test-plugin-session-bindings.lua`:
- Around line 128-132: Add a strict argv length check after grabbing call from
recorded.async_calls: assert that `#call` equals 3 so silent argument drift is
detected; update the test near the existing assertions that reference call and
recorded.async_calls (i.e., after local call =
recorded.async_calls[`#recorded.async_calls`]) to include an assertion that the
argument vector length is exactly 3 with an explanatory message.

In `@src/core/services/tokenizer.test.ts`:
- Around line 3078-3155: Tests for tokenizeSubtitle only cover 'prt' and 'aux-v'
Yomitan wordClasses but the runtime mapping also handles verb, adjective, and
noun-like classes; add additional table-driven test cases using
makeDepsFromYomitanTokens to include one representative token each with
wordClasses like 'v' (or 'vb'), 'adj' (or 'adj-i'), and a noun family (e.g.,
'n'/'noun') and assert tokenizeSubtitle returns the expected PartOfSpeech
(PartOfSpeech.verb, PartOfSpeech.adjective, PartOfSpeech.noun or appropriate
constants), plus correct pos1/pos2 when available; place these alongside the
existing tests referencing tokenizeSubtitle and PartOfSpeech so the mapping
coverage is exercised without changing runtime code.
- Around line 4246-4449: These two filter-focused tests (the ones named
"tokenizeSubtitle clears annotations for ja-nai explanatory endings while
preserving lexical content" and "tokenizeSubtitle clears annotations for
standalone polite copula endings without POS metadata") should be moved out of
the large tokenizer.test.ts and into a new focused test file named
annotation-stage.test.ts; copy the tests verbatim but update their imports to
reference the same helpers (tokenizeSubtitle, makeDepsFromYomitanTokens,
PartOfSpeech) from the module exports used in the original file, ensure any test
helpers remain exported/imported so the tests compile, and run the suite to
confirm the new test file is detected by the runner.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ee2588e5-c33c-491e-854f-98eeb9b483fc

📥 Commits

Reviewing files that changed from the base of the PR and between 03c840c and 738ae17.

⛔ Files ignored due to path filters (20)
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md is excluded by !backlog/**
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
  • backlog/tasks/task-311 - Suppress-auxiliary-inflection-fragments-from-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-312 - Suppress-ja-nai-explanatory-ending-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-315 - Suppress-annotations-for-standalone-じゃない-and-です-ending-tokens.md is excluded by !backlog/**
  • backlog/tasks/task-316 - Fix-macOS-launcher-playback-exit-with-background-stats-daemon.md is excluded by !backlog/**
  • changes/305-tokenizer-word-class-pos-filtering.md is excluded by !changes/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • changes/309-multi-mine-modified-digits.md is excluded by !changes/**
  • changes/311-auxiliary-inflection-annotation-filter.md is excluded by !changes/**
  • changes/312-grammar-ending-annotation-filter.md is excluded by !changes/**
  • changes/316-macos-playback-stats-daemon.md is excluded by !changes/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
📒 Files selected for processing (44)
  • package.json
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/main.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (21)
  • src/core/services/mpv-properties.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/core/services/mpv-protocol.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/style.css
  • src/core/services/overlay-window.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/handlers/keyboard.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • plugin/subminer/session_bindings.lua
  • src/core/services/mpv.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/core/services/mpv.test.ts
  • src/renderer/handlers/mouse.test.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • src/core/services/mpv-protocol.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/stats-server-routing.ts
  • package.json
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/token-merger.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/annotation-stage.test.ts

Comment thread scripts/test-plugin-session-bindings.lua Outdated
Comment thread src/core/services/tokenizer.ts
Comment thread src/core/services/tokenizer/parser-selection-stage.ts Outdated
@ksyasuda ksyasuda force-pushed the tokenizer-updates branch from 738ae17 to 910c240 Compare May 3, 2026 03:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/runtime/mpv-main-event-main-deps.ts`:
- Around line 85-93: The two checks are inconsistent:
hasInitialPlaybackQuitOnDisconnectArg() includes managedPlayback, jellyfinPlay,
and youtubePlay, but shouldQuitOnDisconnectWhenOverlayRuntimeInitialized() only
checks managedPlayback and youtubePlay; update
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized (in
mpv-main-event-main-deps.ts) to use the same predicate as
hasInitialPlaybackQuitOnDisconnectArg by checking
deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay so the overlay-initialized path arms
quit-on-disconnect the same way.

In `@src/renderer/handlers/mouse.test.ts`:
- Around line 1346-1354: The test currently stubs document.elementFromPoint to
always return ctx.dom.subtitleContainer so it doesn't actually validate that
handlePrimaryMouseEnter uses event.clientX/Y; change the stub to inspect its (x,
y) args and return ctx.dom.subtitleContainer only when those coordinates fall
inside the subtitle container's bounds (use
ctx.dom.subtitleContainer.getBoundingClientRect() or an explicit rect on
ctx.dom), otherwise return null; update the similar stub at the other location
mentioned (around the block at 1373-1379) so both tests perform real hit-testing
based on event coordinates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ced4bfa8-fa8b-42c1-b28e-cc1f31ca4b27

📥 Commits

Reviewing files that changed from the base of the PR and between 738ae17 and 910c240.

⛔ Files ignored due to path filters (26)
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md is excluded by !backlog/**
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
  • backlog/tasks/task-311 - Suppress-auxiliary-inflection-fragments-from-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-312 - Suppress-ja-nai-explanatory-ending-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-315 - Suppress-annotations-for-standalone-じゃない-and-です-ending-tokens.md is excluded by !backlog/**
  • backlog/tasks/task-316 - Fix-macOS-launcher-playback-exit-with-background-stats-daemon.md is excluded by !backlog/**
  • backlog/tasks/task-318 - Keep-JLPT-underline-color-fixed-after-lookup-selection.md is excluded by !backlog/**
  • backlog/tasks/task-319 - Suppress-annotations-for-expressive-interjection-subtitles.md is excluded by !backlog/**
  • backlog/tasks/task-320 - Refresh-current-subtitle-known-word-highlight-after-successful-mining.md is excluded by !backlog/**
  • changes/305-tokenizer-word-class-pos-filtering.md is excluded by !changes/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • changes/309-multi-mine-modified-digits.md is excluded by !changes/**
  • changes/311-auxiliary-inflection-annotation-filter.md is excluded by !changes/**
  • changes/312-grammar-ending-annotation-filter.md is excluded by !changes/**
  • changes/316-macos-playback-stats-daemon.md is excluded by !changes/**
  • changes/318-jlpt-underline-selection.md is excluded by !changes/**
  • changes/319-interjection-annotation-filter.md is excluded by !changes/**
  • changes/320-current-subtitle-known-highlight.md is excluded by !changes/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
📒 Files selected for processing (47)
  • package.json
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • src/anki-integration.test.ts
  • src/anki-integration.ts
  • src/anki-integration/known-word-cache.ts
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/main.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (11)
  • src/core/services/overlay-window.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/renderer/handlers/keyboard.test.ts
  • package.json
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/renderer/style.css
  • scripts/test-plugin-session-bindings.lua
  • src/main/runtime/stats-server-routing.test.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv-protocol.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/renderer/subtitle-render.test.ts
  • src/core/services/mpv.ts
  • plugin/subminer/session_bindings.lua
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/renderer/handlers/mouse.ts
  • src/window-trackers/hyprland-tracker.ts
  • src/main/runtime/stats-server-routing.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts

Comment thread src/main/runtime/mpv-main-event-main-deps.ts Outdated
Comment thread src/renderer/handlers/mouse.test.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/core/services/tokenizer/parser-selection-stage.ts (1)

145-150: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't require headwords before splitting standalone endings.

The later split path already falls back to segmentText when no headword is present, so this headword.length > 0 gate makes text-only です / じゃない segments stay merged into the previous token even though YomitanParseSegment.headwords is optional.

Suggested fix
 function isStandaloneGrammarEndingSegment(segment: YomitanParseSegment): boolean {
   const surface = segment.text?.trim() ?? '';
   const headword = extractYomitanHeadword(segment).trim();
-  return (
-    headword.length > 0 &&
-    (isStandaloneGrammarEndingText(surface) || isStandaloneGrammarEndingText(headword))
-  );
+  return isStandaloneGrammarEndingText(surface) || isStandaloneGrammarEndingText(headword);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/parser-selection-stage.ts` around lines 145 -
150, The guard requiring a non-empty headword in
isStandaloneGrammarEndingSegment prevents text-only endings (e.g. "です", "じゃない")
from being recognized; remove the headword.length > 0 check so the function
returns true when either the trimmed segment.text (surface) or the extracted
headword matches via isStandaloneGrammarEndingText, using
extractYomitanHeadword(segment) and segment.text as-is to allow the existing
fallback path to handle missing headwords.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/anki-integration/card-creation.ts`:
- Around line 251-258: The manual-update path is still overwriting expression
audio because sentenceAudioField may be the fallback config.fields.audio even
when the note lacks a sentence-audio field; change the logic so you only write
updatedFields[sentenceAudioField] when that field actually exists on the note.
After computing sentenceAudioField (from getResolvedSentenceAudioFieldName())
add a guard that checks noteInfo.fields.hasOwnProperty(sentenceAudioField) (or
similar existence check) and skip the mergeFieldValue/update if the field is not
present; keep using deps.mergeFieldValue for the real sentence-audio field. Add
a regression test that runs the manual clipboard update on a note missing
SentenceAudio to ensure no ExpressionAudio is overwritten.

In `@src/core/services/tokenizer/subtitle-annotation-filter.ts`:
- Around line 17-20: Remove the literal phrases 'たって' and 'だって' from the broad
blacklist (SUBTITLE_ANNOTATION_EXCLUDED_TERMS) so they don't suppress tokens
whose reading normalizes to those strings; instead rely only on the dedicated
STANDALONE_GRAMMAR_PARTICLE_PHRASES / STANDALONE_GRAMMAR_PARTICLE_PHRASES_SET
and the isStandaloneGrammarParticle() helper to filter real standalone-particle
cases, and keep isExcludedByTerm() behavior unchanged (it should continue to
check both term and reading but no longer match these two phrases via the broad
blacklist).

---

Duplicate comments:
In `@src/core/services/tokenizer/parser-selection-stage.ts`:
- Around line 145-150: The guard requiring a non-empty headword in
isStandaloneGrammarEndingSegment prevents text-only endings (e.g. "です", "じゃない")
from being recognized; remove the headword.length > 0 check so the function
returns true when either the trimmed segment.text (surface) or the extracted
headword matches via isStandaloneGrammarEndingText, using
extractYomitanHeadword(segment) and segment.text as-is to allow the existing
fallback path to handle missing headwords.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d8ed1cb0-6c1f-4a43-a724-cef7d9057eeb

📥 Commits

Reviewing files that changed from the base of the PR and between 910c240 and 59f8de5.

⛔ Files ignored due to path filters (7)
  • backlog/tasks/task-315 - Suppress-annotations-for-standalone-じゃない-and-です-ending-tokens.md is excluded by !backlog/**
  • backlog/tasks/task-321 - Preserve-word-audio-during-manual-clipboard-card-updates.md is excluded by !backlog/**
  • changes/321-grammar-ending-pattern-filter.md is excluded by !changes/**
  • changes/322-preserve-word-audio-manual-update.md is excluded by !changes/**
  • docs-site/anki-integration.md is excluded by !docs-site/**
  • docs-site/configuration.md is excluded by !docs-site/**
  • docs-site/mining-workflow.md is excluded by !docs-site/**
📒 Files selected for processing (8)
  • src/anki-integration/card-creation-manual-update.test.ts
  • src/anki-integration/card-creation.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/grammar-ending.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/services/tokenizer/annotation-stage.ts

Comment thread src/anki-integration/card-creation.ts
Comment thread src/core/services/tokenizer/subtitle-annotation-filter.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/main/runtime/mpv-main-event-main-deps.ts (1)

85-97: ⚡ Quick win

Deduplicate quit-on-disconnect predicate to prevent drift.

Both methods now match (great), but the duplicated condition is easy to desync again. Please extract a shared local predicate and reuse it.

♻️ Proposed refactor
+  const hasInitialPlaybackQuitOnDisconnectArg = (): boolean =>
+    Boolean(
+      deps.appState.initialArgs?.managedPlayback ||
+        deps.appState.initialArgs?.jellyfinPlay ||
+        deps.appState.initialArgs?.youtubePlay,
+    );
+
   return () => ({
@@
-    hasInitialPlaybackQuitOnDisconnectArg: () =>
-      Boolean(
-        deps.appState.initialArgs?.managedPlayback ||
-        deps.appState.initialArgs?.jellyfinPlay ||
-        deps.appState.initialArgs?.youtubePlay,
-      ),
+    hasInitialPlaybackQuitOnDisconnectArg,
@@
-    shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: () =>
-      Boolean(
-        deps.appState.initialArgs?.managedPlayback ||
-        deps.appState.initialArgs?.jellyfinPlay ||
-        deps.appState.initialArgs?.youtubePlay,
-      ),
+    shouldQuitOnDisconnectWhenOverlayRuntimeInitialized:
+      hasInitialPlaybackQuitOnDisconnectArg,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/runtime/mpv-main-event-main-deps.ts` around lines 85 - 97, The two
duplicated predicates in hasInitialPlaybackQuitOnDisconnectArg and
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized should be extracted into a
shared predicate and reused to avoid drift: create a local helper (e.g., const
initialPlaybackQuitOnDisconnect = () =>
Boolean(deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay) or a simple boolean constant computed
once) and replace the existing inline Boolean(...) expressions in both
hasInitialPlaybackQuitOnDisconnectArg and
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized to call/return that shared
symbol; ensure isOverlayRuntimeInitialized remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/runtime/mpv-main-event-main-deps.ts`:
- Around line 85-97: The two duplicated predicates in
hasInitialPlaybackQuitOnDisconnectArg and
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized should be extracted into a
shared predicate and reused to avoid drift: create a local helper (e.g., const
initialPlaybackQuitOnDisconnect = () =>
Boolean(deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay) or a simple boolean constant computed
once) and replace the existing inline Boolean(...) expressions in both
hasInitialPlaybackQuitOnDisconnectArg and
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized to call/return that shared
symbol; ensure isOverlayRuntimeInitialized remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 09f6fde7-e263-4f27-a0e0-3065ef275458

📥 Commits

Reviewing files that changed from the base of the PR and between 59f8de5 and a8e1112.

⛔ Files ignored due to path filters (2)
  • backlog/tasks/task-322 - Fix-failing-CI-checks-on-PR-57.md is excluded by !backlog/**
  • changes/319-interjection-annotation-filter.md is excluded by !changes/**
📒 Files selected for processing (7)
  • scripts/test-plugin-session-bindings.lua
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/subtitle-render.test.ts
✅ Files skipped from review due to trivial changes (1)
  • scripts/test-plugin-session-bindings.lua
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer.ts
  • src/renderer/handlers/mouse.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/subminer/process.lua (1)

298-303: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the startup gate for duplicate auto-starts while readiness is still pending.

Line 302 unconditionally disarms the gate. If file-loaded fires again before subminer-autoplay-ready, this path resumes playback early and bypasses the intended pause-until-ready behavior.

💡 Suggested fix
 		if state.overlay_running then
 			if overrides.auto_start_trigger == true then
 				subminer_log("debug", "process", "Auto-start ignored because overlay is already running")
 				local socket_path = overrides.socket_path or opts.socket_path
-				disarm_auto_play_ready_gate()
+				if not state.auto_play_ready_gate_armed then
+					disarm_auto_play_ready_gate()
+				end
 				local visibility_action = resolve_visible_overlay_startup()
 						and "show-visible-overlay"
 					or "hide-visible-overlay"
 				run_control_command_async(visibility_action, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/subminer/process.lua` around lines 298 - 303, In the branch where
state.overlay_running and overrides.auto_start_trigger are true, do not
unconditionally call disarm_auto_play_ready_gate(); instead first call
resolve_visible_overlay_startup() and inspect its return (e.g.,
visibility_action) and only call disarm_auto_play_ready_gate() when the action
indicates the overlay should start immediately (not when it indicates
defer/wait), so duplicate file-loaded events won't prematurely release the
auto-play gate; update the sequence around state.overlay_running,
overrides.auto_start_trigger, disarm_auto_play_ready_gate(), and
resolve_visible_overlay_startup() accordingly.
🧹 Nitpick comments (2)
src/main/runtime/mpv-main-event-main-deps.ts (1)

85-97: ⚡ Quick win

Deduplicate quit-on-disconnect predicate to prevent future drift.

Both exported checks now match, but they duplicate the same condition and can diverge again during future edits.

♻️ Proposed refactor
+  const hasInitialPlaybackQuitOnDisconnectArg = (): boolean =>
+    Boolean(
+      deps.appState.initialArgs?.managedPlayback ||
+        deps.appState.initialArgs?.jellyfinPlay ||
+        deps.appState.initialArgs?.youtubePlay,
+    );
+
   return () => ({
@@
-    hasInitialPlaybackQuitOnDisconnectArg: () =>
-      Boolean(
-        deps.appState.initialArgs?.managedPlayback ||
-        deps.appState.initialArgs?.jellyfinPlay ||
-        deps.appState.initialArgs?.youtubePlay,
-      ),
+    hasInitialPlaybackQuitOnDisconnectArg,
@@
-    shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: () =>
-      Boolean(
-        deps.appState.initialArgs?.managedPlayback ||
-        deps.appState.initialArgs?.jellyfinPlay ||
-        deps.appState.initialArgs?.youtubePlay,
-      ),
+    shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: hasInitialPlaybackQuitOnDisconnectArg,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/runtime/mpv-main-event-main-deps.ts` around lines 85 - 97, Extract
the duplicated quit-on-disconnect check into a single shared predicate and have
both hasInitialPlaybackQuitOnDisconnectArg and
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized call it; specifically,
create a helper (e.g., initialPlaybackQuitOnDisconnect or
getInitialPlaybackQuitOnDisconnect) that returns
Boolean(deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay) and replace the duplicated inline logic
in both exported checks to call that helper so the condition is defined in one
place and cannot drift.
scripts/test-plugin-start-gate.lua (1)

898-903: ⚡ Quick win

Add a duplicate file-loaded-before-ready regression case.

This scenario currently calls the second file-loaded only after the first ready signal. Add a variant where the second file-loaded fires before subminer-autoplay-ready to protect pause-until-ready semantics from premature gate disarm regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-plugin-start-gate.lua` around lines 898 - 903, Add a regression
variant that fires the second file-loaded before the ready signal to ensure
pause-until-ready semantics hold: duplicate the existing sequence around
fire_event(recorded, "file-loaded") and
recorded.script_messages["subminer-autoplay-ready"]() so that you call
fire_event(recorded, "file-loaded") a second time prior to invoking
recorded.script_messages["subminer-autoplay-ready"](), then invoke the ready
handler and repeat the same assertions; locate the block using fire_event,
recorded, and the "subminer-autoplay-ready" key to insert the new pre-ready
file-loaded case and reuse the same assert_true checks to validate behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@plugin/subminer/process.lua`:
- Around line 298-303: In the branch where state.overlay_running and
overrides.auto_start_trigger are true, do not unconditionally call
disarm_auto_play_ready_gate(); instead first call
resolve_visible_overlay_startup() and inspect its return (e.g.,
visibility_action) and only call disarm_auto_play_ready_gate() when the action
indicates the overlay should start immediately (not when it indicates
defer/wait), so duplicate file-loaded events won't prematurely release the
auto-play gate; update the sequence around state.overlay_running,
overrides.auto_start_trigger, disarm_auto_play_ready_gate(), and
resolve_visible_overlay_startup() accordingly.

---

Nitpick comments:
In `@scripts/test-plugin-start-gate.lua`:
- Around line 898-903: Add a regression variant that fires the second
file-loaded before the ready signal to ensure pause-until-ready semantics hold:
duplicate the existing sequence around fire_event(recorded, "file-loaded") and
recorded.script_messages["subminer-autoplay-ready"]() so that you call
fire_event(recorded, "file-loaded") a second time prior to invoking
recorded.script_messages["subminer-autoplay-ready"](), then invoke the ready
handler and repeat the same assertions; locate the block using fire_event,
recorded, and the "subminer-autoplay-ready" key to insert the new pre-ready
file-loaded case and reuse the same assert_true checks to validate behavior.

In `@src/main/runtime/mpv-main-event-main-deps.ts`:
- Around line 85-97: Extract the duplicated quit-on-disconnect check into a
single shared predicate and have both hasInitialPlaybackQuitOnDisconnectArg and
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized call it; specifically,
create a helper (e.g., initialPlaybackQuitOnDisconnect or
getInitialPlaybackQuitOnDisconnect) that returns
Boolean(deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay) and replace the duplicated inline logic
in both exported checks to call that helper so the condition is defined in one
place and cannot drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 92879433-e70b-44bb-a315-51df020b22e7

📥 Commits

Reviewing files that changed from the base of the PR and between 59f8de5 and b3831b3.

⛔ Files ignored due to path filters (6)
  • backlog/tasks/task-322 - Fix-failing-CI-checks-on-PR-57.md is excluded by !backlog/**
  • backlog/tasks/task-323 - Fix-macOS-overlay-hiding-while-mpv-remains-active.md is excluded by !backlog/**
  • backlog/tasks/task-324 - Fix-mpv-playlist-changes-re-running-app-warmups.md is excluded by !backlog/**
  • changes/319-interjection-annotation-filter.md is excluded by !changes/**
  • changes/323-macos-overlay-tracker-flaps.md is excluded by !changes/**
  • changes/324-mpv-playlist-overlay-reuse.md is excluded by !changes/**
📒 Files selected for processing (11)
  • plugin/subminer/process.lua
  • scripts/test-plugin-session-bindings.lua
  • scripts/test-plugin-start-gate.lua
  • src/core/services/overlay-visibility.test.ts
  • src/core/services/overlay-visibility.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/subtitle-render.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/renderer/handlers/mouse.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/subminer/process.lua (1)

298-303: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t clear an active startup gate on duplicate auto-start.

When a duplicate file-loaded arrives before readiness, Line 302 disarms the currently armed gate and resumes playback early. That bypasses auto_start_pause_until_ready for the active startup cycle.

Proposed fix
 		if state.overlay_running then
 			if overrides.auto_start_trigger == true then
 				subminer_log("debug", "process", "Auto-start ignored because overlay is already running")
 				local socket_path = overrides.socket_path or opts.socket_path
-				disarm_auto_play_ready_gate()
+				-- Preserve an active startup gate; only clear stale/inactive state.
+				if not state.auto_play_ready_gate_armed then
+					disarm_auto_play_ready_gate()
+				end
 				local visibility_action = resolve_visible_overlay_startup()
 						and "show-visible-overlay"
 					or "hide-visible-overlay"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/subminer/process.lua` around lines 298 - 303, The current branch
disarms the startup gate unconditionally when a duplicate auto-start arrives
while state.overlay_running, which lets playback resume early; change the logic
so disarm_auto_play_ready_gate() is not called for duplicate triggers when
state.overlay_running is true. Specifically, in the block handling
overrides.auto_start_trigger and state.overlay_running, remove or skip the call
to disarm_auto_play_ready_gate() (keep the log and socket_path resolution), and
only call disarm_auto_play_ready_gate() from the path that actually starts a new
overlay (the branch where state.overlay_running is false or where you initiate
startup), ensuring auto_start_pause_until_ready remains enforced for the active
startup cycle.
🧹 Nitpick comments (3)
src/renderer/subtitle-render.test.ts (1)

910-912: ⚡ Quick win

Harden the “no underline” guard to catch shorthand text-decoration.

Current check blocks text-decoration-line: underline but still allows text-decoration: underline .... Add one more negative assertion so this regression can’t slip through.

Proposed test hardening
     assert.doesNotMatch(plainJlptBlock, /(?:^|\n)\s*color\s*:/m);
     assert.doesNotMatch(plainJlptBlock, /text-decoration-line:\s*underline;/);
+    assert.doesNotMatch(plainJlptBlock, /text-decoration\s*:[^;]*\bunderline\b/i);
     assert.match(
       plainJlptBlock,
       new RegExp(`border-bottom:\\s*2px\\s+solid\\s+var\\(--subtitle-jlpt-n${level}-color,`),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/subtitle-render.test.ts` around lines 910 - 912, The test
currently ensures no 'text-decoration-line: underline' but misses shorthand
'text-decoration: underline'; update the assertions around plainJlptBlock (used
in subtitle-render.test) to also assert.doesNotMatch(plainJlptBlock,
/(?:^|\n)\s*text-decoration\s*:/) or a regex specifically blocking
'text-decoration:\s*underline' so any shorthand underline usage is rejected;
modify the block where assert.doesNotMatch(plainJlptBlock,
/text-decoration-line:\s*underline;/) appears to include this additional
negative assertion referencing plainJlptBlock.
src/main/runtime/mpv-main-event-actions.test.ts (1)

196-224: ⚡ Quick win

Add a rejection-path test for the new post-watch error handler.

This test only validates the success path. Please also assert that a rejected maybeRunAnilistPostWatchUpdate calls logError and does not break subsequent handler behavior.

Proposed test addition
+test('time-pos handler logs when post-watch update rejects', async () => {
+  const calls: string[] = [];
+  const timeHandler = createHandleMpvTimePosChangeHandler({
+    recordPlaybackPosition: (time) => calls.push(`time:${time}`),
+    reportJellyfinRemoteProgress: (force) => calls.push(`progress:${force ? 'force' : 'normal'}`),
+    refreshDiscordPresence: () => calls.push('presence'),
+    maybeRunAnilistPostWatchUpdate: async () => {
+      throw new Error('boom');
+    },
+    logError: () => calls.push('post-watch-error'),
+  });
+
+  timeHandler({ time: 12.5 });
+  await Promise.resolve(); // allow catch microtask to run
+
+  assert.deepEqual(calls, ['time:12.5', 'progress:normal', 'presence', 'post-watch-error']);
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/runtime/mpv-main-event-actions.test.ts` around lines 196 - 224, The
test currently only covers the success path for
createHandleMpvTimePosChangeHandler's maybeRunAnilistPostWatchUpdate; add a
counterpart that simulates maybeRunAnilistPostWatchUpdate rejecting and assert
that the supplied logError is called and the rest of the handlers still run in
order. Specifically, create a test that supplies a
maybeRunAnilistPostWatchUpdate which returns a rejected promise, a logError spy
that records calls, then invoke timeHandler({ time: ... }) followed by
pauseHandler({ paused: ... }) and assert the calls array includes the
'post-watch-error' entry (or that logError was called) in place of the success
'post-watch' and that subsequent progress/presence/pause handlers still execute
as before; use the same helper factories createHandleMpvTimePosChangeHandler and
createHandleMpvPauseChangeHandler to locate the code under test.
scripts/test-plugin-start-gate.lua (1)

909-925: ⚡ Quick win

Add a pre-ready duplicate case to lock gate semantics.

This scenario only tests a duplicate file-loaded after the first ready signal. Add a sibling case where the second file-loaded happens before subminer-autoplay-ready, then assert pause remains armed until the first ready event.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-plugin-start-gate.lua` around lines 909 - 925, Add a sibling
test case in scripts/test-plugin-start-gate.lua that simulates a second
"file-loaded" firing before the first "subminer-autoplay-ready" to verify that
the pause-until-ready gate stays armed until the ready event; locate the
existing duplicate case near the assertions using helpers like
count_osd_message(recorded.osd, ...), count_property_set(recorded.property_sets,
"pause", ...), and the event names "file-loaded" and "subminer-autoplay-ready",
then create a new test sequence that triggers: initial file-loaded -> pause
should be set true, trigger a duplicate file-loaded (pre-ready) -> assert pause
remains true and no extra ready/tokenization messages are emitted yet, then emit
subminer-autoplay-ready -> assert tokenization ready message appears once and
pause is cleared once (count_property_set "pause", false == 1).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/anilist/anilist-updater.ts`:
- Around line 239-245: The current return unconditionally substitutes
parseMediaInfo(target) values for season/episode when guessit (season/episode)
are undefined; instead, gate use of fallback.season and fallback.episode by the
parser confidence—only apply fallback values when parseMediaInfo(target)
indicates high confidence (e.g., confidence >= a safe threshold or a
highConfidence flag). Update the logic around the return in anilist-updater.ts
(the block that calls parseMediaInfo, buildGuessitTitle and returns
title/alternativeTitle/year/season/episode) to check fallback.confidence (or
equivalent) before assigning fallback.season/fallback.episode so low-confidence
parser guesses do not bypass the downstream guard in anilist-post-watch.ts.

---

Outside diff comments:
In `@plugin/subminer/process.lua`:
- Around line 298-303: The current branch disarms the startup gate
unconditionally when a duplicate auto-start arrives while state.overlay_running,
which lets playback resume early; change the logic so
disarm_auto_play_ready_gate() is not called for duplicate triggers when
state.overlay_running is true. Specifically, in the block handling
overrides.auto_start_trigger and state.overlay_running, remove or skip the call
to disarm_auto_play_ready_gate() (keep the log and socket_path resolution), and
only call disarm_auto_play_ready_gate() from the path that actually starts a new
overlay (the branch where state.overlay_running is false or where you initiate
startup), ensuring auto_start_pause_until_ready remains enforced for the active
startup cycle.

---

Nitpick comments:
In `@scripts/test-plugin-start-gate.lua`:
- Around line 909-925: Add a sibling test case in
scripts/test-plugin-start-gate.lua that simulates a second "file-loaded" firing
before the first "subminer-autoplay-ready" to verify that the pause-until-ready
gate stays armed until the ready event; locate the existing duplicate case near
the assertions using helpers like count_osd_message(recorded.osd, ...),
count_property_set(recorded.property_sets, "pause", ...), and the event names
"file-loaded" and "subminer-autoplay-ready", then create a new test sequence
that triggers: initial file-loaded -> pause should be set true, trigger a
duplicate file-loaded (pre-ready) -> assert pause remains true and no extra
ready/tokenization messages are emitted yet, then emit subminer-autoplay-ready
-> assert tokenization ready message appears once and pause is cleared once
(count_property_set "pause", false == 1).

In `@src/main/runtime/mpv-main-event-actions.test.ts`:
- Around line 196-224: The test currently only covers the success path for
createHandleMpvTimePosChangeHandler's maybeRunAnilistPostWatchUpdate; add a
counterpart that simulates maybeRunAnilistPostWatchUpdate rejecting and assert
that the supplied logError is called and the rest of the handlers still run in
order. Specifically, create a test that supplies a
maybeRunAnilistPostWatchUpdate which returns a rejected promise, a logError spy
that records calls, then invoke timeHandler({ time: ... }) followed by
pauseHandler({ paused: ... }) and assert the calls array includes the
'post-watch-error' entry (or that logError was called) in place of the success
'post-watch' and that subsequent progress/presence/pause handlers still execute
as before; use the same helper factories createHandleMpvTimePosChangeHandler and
createHandleMpvPauseChangeHandler to locate the code under test.

In `@src/renderer/subtitle-render.test.ts`:
- Around line 910-912: The test currently ensures no 'text-decoration-line:
underline' but misses shorthand 'text-decoration: underline'; update the
assertions around plainJlptBlock (used in subtitle-render.test) to also
assert.doesNotMatch(plainJlptBlock, /(?:^|\n)\s*text-decoration\s*:/) or a regex
specifically blocking 'text-decoration:\s*underline' so any shorthand underline
usage is rejected; modify the block where assert.doesNotMatch(plainJlptBlock,
/text-decoration-line:\s*underline;/) appears to include this additional
negative assertion referencing plainJlptBlock.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e173e4ec-d560-4c1e-95ac-42223551abd9

📥 Commits

Reviewing files that changed from the base of the PR and between 59f8de5 and a143ad1.

⛔ Files ignored due to path filters (14)
  • backlog/tasks/task-322 - Fix-failing-CI-checks-on-PR-57.md is excluded by !backlog/**
  • backlog/tasks/task-323 - Fix-macOS-overlay-hiding-while-mpv-remains-active.md is excluded by !backlog/**
  • backlog/tasks/task-324 - Fix-mpv-playlist-changes-re-running-app-warmups.md is excluded by !backlog/**
  • backlog/tasks/task-325 - Keep-JLPT-underline-color-fixed-with-combined-lookup-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-326 - Fix-AniList-post-watch-update-after-skipped-completion-threshold.md is excluded by !backlog/**
  • backlog/tasks/task-327 - Restore-stats-daemon-deferral-when-launching-playback.md is excluded by !backlog/**
  • backlog/tasks/task-328 - Keep-subtitle-prefetch-running-after-immediate-cached-annotation-render.md is excluded by !backlog/**
  • changes/319-interjection-annotation-filter.md is excluded by !changes/**
  • changes/323-macos-overlay-tracker-flaps.md is excluded by !changes/**
  • changes/324-mpv-playlist-overlay-reuse.md is excluded by !changes/**
  • changes/325-jlpt-combined-annotation-underlines.md is excluded by !changes/**
  • changes/326-anilist-time-position-post-watch.md is excluded by !changes/**
  • changes/327-stats-daemon-deferral.md is excluded by !changes/**
  • changes/328-subtitle-prefetch-cache-hit-resume.md is excluded by !changes/**
📒 Files selected for processing (19)
  • plugin/subminer/process.lua
  • scripts/test-plugin-session-bindings.lua
  • scripts/test-plugin-start-gate.lua
  • src/core/services/anilist/anilist-updater.test.ts
  • src/core/services/anilist/anilist-updater.ts
  • src/core/services/overlay-visibility.test.ts
  • src/core/services/overlay-visibility.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/main/runtime/mpv-main-event-actions.test.ts
  • src/main/runtime/mpv-main-event-actions.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/core/services/tokenizer/annotation-stage.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/renderer/handlers/mouse.test.ts
  • src/core/services/tokenizer.ts
  • scripts/test-plugin-session-bindings.lua

Comment thread src/core/services/anilist/anilist-updater.ts Outdated
@ksyasuda ksyasuda force-pushed the tokenizer-updates branch from a143ad1 to 5f7c5f0 Compare May 4, 2026 04:27
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/anki-integration/known-word-cache.ts (1)

174-190: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return value can miss cache-reset mutations.

Line 174 can clear cache state, but Lines 183/189 may still return false. Since downstream notifications now key off this boolean, real cache changes can be dropped.

Suggested fix
   appendFromNoteInfo(noteInfo: KnownWordCacheNoteInfo): boolean {
     if (!this.isKnownWordCacheEnabled() || !this.shouldAddMinedWordsImmediately()) {
       return false;
     }

+    let didMutateCache = false;
     const currentStateKey = this.getKnownWordCacheStateKey();
     if (this.knownWordsStateKey && this.knownWordsStateKey !== currentStateKey) {
+      didMutateCache = this.knownWords.size > 0 || this.noteWordsById.size > 0;
       this.clearKnownWordCacheState();
     }
     if (!this.knownWordsStateKey) {
       this.knownWordsStateKey = currentStateKey;
     }

     const preferredFields = this.getImmediateAppendFields();
     if (!preferredFields) {
-      return false;
+      return didMutateCache;
     }

     const nextWords = this.extractNormalizedKnownWordsFromNoteInfo(noteInfo, preferredFields);
     const changed = this.replaceNoteSnapshot(noteInfo.noteId, nextWords);
     if (!changed) {
-      return false;
+      return didMutateCache;
     }

     if (this.knownWordsLastRefreshedAtMs <= 0) {
       this.knownWordsLastRefreshedAtMs = Date.now();
     }
@@
-    return true;
+    return true;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/anki-integration/known-word-cache.ts` around lines 174 - 190, The code
may clear cache state via this.clearKnownWordCacheState() but still return false
later, dropping a real mutation; modify the logic in the method that contains
this.knownWordsStateKey checks so the result reflects cache mutations: introduce
a local changed flag (or set changed = true when this.clearKnownWordCacheState()
runs), then continue to compute nextWords and call
this.replaceNoteSnapshot(noteInfo.noteId, nextWords) and return the boolean OR
of the clear-induced change and replaceNoteSnapshot result. Ensure references to
knownWordsStateKey, clearKnownWordCacheState(), getImmediateAppendFields(),
extractNormalizedKnownWordsFromNoteInfo(), and replaceNoteSnapshot() are used to
locate and update the code.
src/main/runtime/anilist-post-watch.ts (1)

125-172: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Acquire inFlight before async gating to prevent duplicate AniList writes.

At Line 126 you read getInFlight(), but setInFlight(true) is only set at Line 172 after awaited work. Two concurrent calls can both pass the guard and race into duplicate AniList updates.

Proposed fix
 export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
@@
 }) {
   return async (options: AnilistPostWatchRunOptions = {}): Promise<void> => {
     if (deps.getInFlight()) {
       return;
     }
+    deps.setInFlight(true);
-    const force = options.force === true;
-
-    const resolved = deps.getResolvedConfig();
-    if (!deps.isAnilistTrackingEnabled(resolved)) {
-      return;
-    }
+    try {
+      const force = options.force === true;
+
+      const resolved = deps.getResolvedConfig();
+      if (!deps.isAnilistTrackingEnabled(resolved)) {
+        return;
+      }
@@
-    deps.setInFlight(true);
-    try {
       await deps.processNextAnilistRetryUpdate();
@@
-    } finally {
-      deps.setInFlight(false);
-    }
+    } finally {
+      deps.setInFlight(false);
+    }
   };
 }
🧹 Nitpick comments (1)
src/core/services/ipc.test.ts (1)

305-351: ⚡ Quick win

Add a rejection-path test for the manual-mark AniList callback.

Please add a case where runAnilistPostWatchUpdateOnManualMark throws and verify the handler still returns true for a successful mark. This will lock in failure isolation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/ipc.test.ts` around lines 305 - 351, Add a new test that
verifies when markActiveVideoWatched returns true but
runAnilistPostWatchUpdateOnManualMark throws, the IPC handler still returns true
and the thrown error is isolated; use the existing test setup
(createFakeIpcRegistrar, createRegisterIpcDeps, createFakeImmersionTracker) but
make runAnilistPostWatchUpdateOnManualMark async () => { throw new Error('..') }
and assert the result from
handlers.handle.get(IPC_CHANNELS.command.markActiveVideoWatched)?.({}) is true
and that calls include 'mark' (and optionally ensure 'anilist' is not required);
place this alongside the other registerIpcHandlers tests so it covers the
rejection path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/ipc.ts`:
- Around line 390-395: The IPC handler for
IPC_CHANNELS.command.markActiveVideoWatched currently lets exceptions from
deps.runAnilistPostWatchUpdateOnManualMark propagate and fail the whole call;
change the handler so that after awaiting
deps.immersionTracker?.markActiveVideoWatched() you still return the boolean
result even if the post-watch sync fails by wrapping the call to
runAnilistPostWatchUpdateOnManualMark in a try/catch (or otherwise handling
rejection) and logging the error (do not rethrow), so markActiveVideoWatched()
success is never invalidated by runAnilistPostWatchUpdateOnManualMark failures.

---

Outside diff comments:
In `@src/anki-integration/known-word-cache.ts`:
- Around line 174-190: The code may clear cache state via
this.clearKnownWordCacheState() but still return false later, dropping a real
mutation; modify the logic in the method that contains this.knownWordsStateKey
checks so the result reflects cache mutations: introduce a local changed flag
(or set changed = true when this.clearKnownWordCacheState() runs), then continue
to compute nextWords and call this.replaceNoteSnapshot(noteInfo.noteId,
nextWords) and return the boolean OR of the clear-induced change and
replaceNoteSnapshot result. Ensure references to knownWordsStateKey,
clearKnownWordCacheState(), getImmediateAppendFields(),
extractNormalizedKnownWordsFromNoteInfo(), and replaceNoteSnapshot() are used to
locate and update the code.

---

Nitpick comments:
In `@src/core/services/ipc.test.ts`:
- Around line 305-351: Add a new test that verifies when markActiveVideoWatched
returns true but runAnilistPostWatchUpdateOnManualMark throws, the IPC handler
still returns true and the thrown error is isolated; use the existing test setup
(createFakeIpcRegistrar, createRegisterIpcDeps, createFakeImmersionTracker) but
make runAnilistPostWatchUpdateOnManualMark async () => { throw new Error('..') }
and assert the result from
handlers.handle.get(IPC_CHANNELS.command.markActiveVideoWatched)?.({}) is true
and that calls include 'mark' (and optionally ensure 'anilist' is not required);
place this alongside the other registerIpcHandlers tests so it covers the
rejection path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2af1ebd9-8289-4fed-8d46-38f01e435574

📥 Commits

Reviewing files that changed from the base of the PR and between a143ad1 and 5f7c5f0.

⛔ Files ignored due to path filters (48)
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md is excluded by !backlog/**
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
  • backlog/tasks/task-311 - Suppress-auxiliary-inflection-fragments-from-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-312 - Suppress-ja-nai-explanatory-ending-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-315 - Suppress-annotations-for-standalone-じゃない-and-です-ending-tokens.md is excluded by !backlog/**
  • backlog/tasks/task-316 - Fix-macOS-launcher-playback-exit-with-background-stats-daemon.md is excluded by !backlog/**
  • backlog/tasks/task-318 - Keep-JLPT-underline-color-fixed-after-lookup-selection.md is excluded by !backlog/**
  • backlog/tasks/task-319 - Suppress-annotations-for-expressive-interjection-subtitles.md is excluded by !backlog/**
  • backlog/tasks/task-320 - Refresh-current-subtitle-known-word-highlight-after-successful-mining.md is excluded by !backlog/**
  • backlog/tasks/task-321 - Preserve-word-audio-during-manual-clipboard-card-updates.md is excluded by !backlog/**
  • backlog/tasks/task-322 - Fix-failing-CI-checks-on-PR-57.md is excluded by !backlog/**
  • backlog/tasks/task-323 - Fix-macOS-overlay-hiding-while-mpv-remains-active.md is excluded by !backlog/**
  • backlog/tasks/task-324 - Fix-mpv-playlist-changes-re-running-app-warmups.md is excluded by !backlog/**
  • backlog/tasks/task-325 - Keep-JLPT-underline-color-fixed-with-combined-lookup-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-326 - Fix-AniList-post-watch-update-after-skipped-completion-threshold.md is excluded by !backlog/**
  • backlog/tasks/task-327 - Restore-stats-daemon-deferral-when-launching-playback.md is excluded by !backlog/**
  • backlog/tasks/task-328 - Keep-subtitle-prefetch-running-after-immediate-cached-annotation-render.md is excluded by !backlog/**
  • backlog/tasks/task-331 - Address-unresolved-CodeRabbit-comments-on-PR-57.md is excluded by !backlog/**
  • backlog/tasks/task-332 - Fix-subtitle-frequency-annotation-missing-ranks-shown-in-Yomitan-popup.md is excluded by !backlog/**
  • changes/305-tokenizer-word-class-pos-filtering.md is excluded by !changes/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • changes/309-multi-mine-modified-digits.md is excluded by !changes/**
  • changes/311-auxiliary-inflection-annotation-filter.md is excluded by !changes/**
  • changes/312-grammar-ending-annotation-filter.md is excluded by !changes/**
  • changes/316-macos-playback-stats-daemon.md is excluded by !changes/**
  • changes/318-jlpt-underline-selection.md is excluded by !changes/**
  • changes/319-interjection-annotation-filter.md is excluded by !changes/**
  • changes/320-current-subtitle-known-highlight.md is excluded by !changes/**
  • changes/321-grammar-ending-pattern-filter.md is excluded by !changes/**
  • changes/322-preserve-word-audio-manual-update.md is excluded by !changes/**
  • changes/323-macos-overlay-tracker-flaps.md is excluded by !changes/**
  • changes/324-mpv-playlist-overlay-reuse.md is excluded by !changes/**
  • changes/325-jlpt-combined-annotation-underlines.md is excluded by !changes/**
  • changes/326-anilist-time-position-post-watch.md is excluded by !changes/**
  • changes/327-stats-daemon-deferral.md is excluded by !changes/**
  • changes/328-subtitle-prefetch-cache-hit-resume.md is excluded by !changes/**
  • changes/332-subtitle-frequency-ordinal-prefix.md is excluded by !changes/**
  • docs-site/anki-integration.md is excluded by !docs-site/**
  • docs-site/configuration.md is excluded by !docs-site/**
  • docs-site/mining-workflow.md is excluded by !docs-site/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
📒 Files selected for processing (64)
  • package.json
  • plugin/subminer/lifecycle.lua
  • plugin/subminer/process.lua
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • scripts/test-plugin-start-gate.lua
  • src/anki-integration.test.ts
  • src/anki-integration.ts
  • src/anki-integration/card-creation-manual-update.test.ts
  • src/anki-integration/card-creation.ts
  • src/anki-integration/known-word-cache.ts
  • src/core/services/anilist/anilist-updater.test.ts
  • src/core/services/anilist/anilist-updater.ts
  • src/core/services/ipc.test.ts
  • src/core/services/ipc.ts
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-visibility.test.ts
  • src/core/services/overlay-visibility.ts
  • src/core/services/overlay-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/grammar-ending.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/main.ts
  • src/main/dependencies.ts
  • src/main/runtime/anilist-post-watch.test.ts
  • src/main/runtime/anilist-post-watch.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-actions.test.ts
  • src/main/runtime/mpv-main-event-actions.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
✅ Files skipped from review due to trivial changes (19)
  • package.json
  • src/core/services/mpv.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/core/services/overlay-visibility.test.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/mpv.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/token-merger.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/window-trackers/hyprland-tracker.ts
🚧 Files skipped from review as they are similar to previous changes (21)
  • src/main/runtime/stats-server-routing.test.ts
  • scripts/test-plugin-session-bindings.lua
  • src/anki-integration/card-creation-manual-update.test.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • plugin/subminer/session_bindings.lua
  • src/core/services/overlay-window.ts
  • src/anki-integration.test.ts
  • src/renderer/handlers/mouse.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/anki-integration/card-creation.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/mpv-main-event-actions.ts
  • src/core/services/anilist/anilist-updater.test.ts
  • src/renderer/style.css
  • src/core/services/anilist/anilist-updater.ts
  • plugin/subminer/process.lua
  • src/anki-integration.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts

Comment thread src/core/services/ipc.ts
ksyasuda added a commit that referenced this pull request May 4, 2026
- 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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/core/services/tokenizer/annotation-stage.test.ts (1)

518-679: 🏗️ Heavy lift

Split this new subtitle-filter matrix into focused or table-driven tests.

These additions are valuable, but keeping dozens of near-identical exclusion/preservation cases inline in one file will make future failures noisy and the file harder to navigate. Please move this matrix into a dedicated subtitle-filter spec or collapse the repeated setup/assertions behind shared helpers. As per coding guidelines, **/*.{ts,tsx}: TypeScript files should be kept small and follow existing patterns.

Also applies to: 814-1032, 1361-1854

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/annotation-stage.test.ts` around lines 518 - 679,
The new long matrix of near-duplicate tests for
shouldExcludeTokenFromSubtitleAnnotations should be extracted into a focused,
table-driven spec (e.g., subtitle-filter or subtitle-filter.spec) or collapsed
behind a shared helper; create a new test file and move these cases (including
the loops and uses of makeToken and assert.equal) into a single data-driven test
that iterates an array of {surface, headword, reading, partOfSpeech, pos1, pos2,
expected} entries, reusing makeToken and a small assertion helper, then delete
the inlined blocks from this file to keep each TypeScript test file small and
consistent with existing patterns.
src/core/services/tokenizer.test.ts (1)

3078-3155: 🏗️ Heavy lift

Break these new tokenizer regressions into focused suites.

The new coverage is good, but adding it here pushes even more unrelated scenarios into an already very large spec. Please split the word-class, subtitle-filter, and N+1 regressions into dedicated test files/helpers so the main tokenizer test remains navigable and failures map to one concern at a time. As per coding guidelines, **/*.{ts,tsx}: TypeScript files should be kept small and follow existing patterns.

Also applies to: 3233-3285, 4309-4512, 4733-4977

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer.test.ts` around lines 3078 - 3155, The test file
has grown with multiple unrelated tokenizer scenarios—split the new word-class,
subtitle-filter, and N+1 regression tests into focused suites: create new test
files (e.g., tokenizer.wordclass.test.ts, tokenizer.subtitle-filter.test.ts,
tokenizer.nplusone.test.ts) and move the tests that call tokenizeSubtitle and
makeDepsFromYomitanTokens (the three shown and the other ranges noted) into the
appropriate new files; keep assertions referencing PartOfSpeech,
tokenizeSubtitle, and makeDepsFromYomitanTokens intact, and update any
imports/exports so each new file only contains the related tests to match
existing test patterns and keep each spec small and focused.
src/core/services/tokenizer/annotation-stage.ts (1)

152-203: 🏗️ Heavy lift

Extract the frequency exception rules out of annotation-stage.ts.

shouldAllowOrdinalPrefixNounFrequency is another surface/POS-specific escape hatch in a file that already owns annotation orchestration, JLPT lookup, known-word resolution, and subtitle filtering. Please move the frequency-allowance heuristics into a dedicated filter/rule module so future exceptions stay local and testable. As per coding guidelines, **/*.{ts,tsx}: TypeScript files should be kept small and follow existing patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/tokenizer/annotation-stage.ts` around lines 152 - 203, The
frequency-exemption heuristics (e.g., shouldAllowOrdinalPrefixNounFrequency)
need to be extracted from annotation-stage.ts into a dedicated module (suggest
name: frequencyRules or frequencyExemptions) so these surface/POS-specific
allowlists are isolated and testable; move shouldAllowOrdinalPrefixNounFrequency
and any related helpers (and their dependencies like splitNormalizedTagParts,
normalizePos1Tag/normalizePos2Tag usages) into that module, export a single
entrypoint (e.g., allowFrequencyForToken(token, pos1Exclusions, pos2Exclusions)
or specific predicate functions) and update isFrequencyExcludedByPos to call the
new exported function(s) instead of the in-file implementation while keeping
existing calls to isSingleKanaFrequencyNoiseToken,
shouldAllowContentLedMergedTokenFrequency, and isExcludedByTagSet unaffected;
add unit tests for the new module covering ordinal-prefix and content-led
merged-token cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/ipc.ts`:
- Around line 395-399: The catch block currently accesses (error as
Error).message which can throw if error is null/undefined; update the catch in
the markActiveVideoWatched/related handler in ipc.ts to safely derive the
message (e.g., check error instanceof Error and use error.message, otherwise
stringify or log the error value) and pass that safe string or the original
error to console.warn so logging itself cannot throw.

---

Nitpick comments:
In `@src/core/services/tokenizer.test.ts`:
- Around line 3078-3155: The test file has grown with multiple unrelated
tokenizer scenarios—split the new word-class, subtitle-filter, and N+1
regression tests into focused suites: create new test files (e.g.,
tokenizer.wordclass.test.ts, tokenizer.subtitle-filter.test.ts,
tokenizer.nplusone.test.ts) and move the tests that call tokenizeSubtitle and
makeDepsFromYomitanTokens (the three shown and the other ranges noted) into the
appropriate new files; keep assertions referencing PartOfSpeech,
tokenizeSubtitle, and makeDepsFromYomitanTokens intact, and update any
imports/exports so each new file only contains the related tests to match
existing test patterns and keep each spec small and focused.

In `@src/core/services/tokenizer/annotation-stage.test.ts`:
- Around line 518-679: The new long matrix of near-duplicate tests for
shouldExcludeTokenFromSubtitleAnnotations should be extracted into a focused,
table-driven spec (e.g., subtitle-filter or subtitle-filter.spec) or collapsed
behind a shared helper; create a new test file and move these cases (including
the loops and uses of makeToken and assert.equal) into a single data-driven test
that iterates an array of {surface, headword, reading, partOfSpeech, pos1, pos2,
expected} entries, reusing makeToken and a small assertion helper, then delete
the inlined blocks from this file to keep each TypeScript test file small and
consistent with existing patterns.

In `@src/core/services/tokenizer/annotation-stage.ts`:
- Around line 152-203: The frequency-exemption heuristics (e.g.,
shouldAllowOrdinalPrefixNounFrequency) need to be extracted from
annotation-stage.ts into a dedicated module (suggest name: frequencyRules or
frequencyExemptions) so these surface/POS-specific allowlists are isolated and
testable; move shouldAllowOrdinalPrefixNounFrequency and any related helpers
(and their dependencies like splitNormalizedTagParts,
normalizePos1Tag/normalizePos2Tag usages) into that module, export a single
entrypoint (e.g., allowFrequencyForToken(token, pos1Exclusions, pos2Exclusions)
or specific predicate functions) and update isFrequencyExcludedByPos to call the
new exported function(s) instead of the in-file implementation while keeping
existing calls to isSingleKanaFrequencyNoiseToken,
shouldAllowContentLedMergedTokenFrequency, and isExcludedByTagSet unaffected;
add unit tests for the new module covering ordinal-prefix and content-led
merged-token cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5b4eac4-9ec3-4032-acd7-7bd3bcbaee60

📥 Commits

Reviewing files that changed from the base of the PR and between 5f7c5f0 and 53c78f2.

⛔ Files ignored due to path filters (4)
  • backlog/tasks/task-333 - Suppress-aru-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-334 - Assess-and-address-PR-57-latest-CodeRabbit-comments.md is excluded by !backlog/**
  • changes/333-aru-annotation-filter.md is excluded by !changes/**
  • changes/334-coderabbit-followups.md is excluded by !changes/**
📒 Files selected for processing (10)
  • src/anki-integration/known-word-cache.test.ts
  • src/anki-integration/known-word-cache.ts
  • src/core/services/ipc.test.ts
  • src/core/services/ipc.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/main/runtime/anilist-post-watch.test.ts
  • src/main/runtime/anilist-post-watch.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/runtime/anilist-post-watch.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/anki-integration/known-word-cache.ts
  • src/core/services/ipc.test.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts

Comment thread src/core/services/ipc.ts
Comment on lines +395 to +399
} catch (error) {
console.warn(
'Failed to run AniList post-watch update after manual watched mark:',
(error as Error).message,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Harden catch logging to avoid re-throwing on non-Error values.

At Line 398, (error as Error).message can itself throw when error is null/undefined, which can still fail markActiveVideoWatched despite the try/catch intent.

Proposed fix
       } catch (error) {
+        const errorMessage = error instanceof Error ? error.message : String(error);
         console.warn(
           'Failed to run AniList post-watch update after manual watched mark:',
-          (error as Error).message,
+          errorMessage,
         );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/ipc.ts` around lines 395 - 399, The catch block currently
accesses (error as Error).message which can throw if error is null/undefined;
update the catch in the markActiveVideoWatched/related handler in ipc.ts to
safely derive the message (e.g., check error instanceof Error and use
error.message, otherwise stringify or log the error value) and pass that safe
string or the original error to console.warn so logging itself cannot throw.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/hyprland-window-placement.ts`:
- Around line 135-151: The synchronous hyprctl calls use the local alias run
(set from options.execFileSync ?? execFileSync) without a timeout, so wrap both
invocations—when fetching clients (run('hyprctl', ['-j', 'clients'], { encoding:
'utf-8' })) and when dispatching placement args (run('hyprctl', args, { stdio:
'ignore' }))—to include a timeout option (e.g., timeout: <ms>) to avoid blocking
the Electron main thread; add a clear constant name (e.g., HYPRCTL_TIMEOUT_MS)
if one doesn’t exist and pass it into both run calls, preserving any existing
options (encoding/stdio) while merging the timeout, and apply the same pattern
in findHyprlandWindowForPlacement call sites and the similar calls in
src/window-trackers/hyprland-tracker.ts.

In `@src/core/services/tokenizer/annotation-stage.ts`:
- Around line 624-631: The code unconditionally clears known-state by returning
{ ...strippedToken, isKnown: false } after calling
sharedStripSubtitleAnnotationMetadata, which strips annotation metadata for
excluded kanji tokens; instead preserve the original token's known state. Fix by
returning strippedToken as-is (or explicitly set isKnown to token.isKnown)
rather than forcing false—update the return near the call to
sharedStripSubtitleAnnotationMetadata (variables: token, strippedToken, property
isKnown).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e204c32e-6232-479c-9d34-65b5134616ff

📥 Commits

Reviewing files that changed from the base of the PR and between 5b9bbae and cea3c20.

⛔ Files ignored due to path filters (15)
  • backlog/completed/task-336 - Fix-Hyprland-fullscreen-overlay-downward-offset.md is excluded by !backlog/**
  • backlog/completed/task-339 - Stop-pinning-Hyprland-overlay-windows.md is excluded by !backlog/**
  • backlog/tasks/task-337 - Fix-transient-Linux-safeStorage-failure-poisoning-AniList-token-store.md is excluded by !backlog/**
  • backlog/tasks/task-338 - Fix-known-word-highlight-on-standalone-subtitle-particles.md is excluded by !backlog/**
  • backlog/tasks/task-340 - Restore-default-replay-and-next-subtitle-overlay-keybindings.md is excluded by !backlog/**
  • changes/336-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/336-overlay-focus-ring.md is excluded by !changes/**
  • changes/337-anilist-safe-storage-retry.md is excluded by !changes/**
  • changes/338-known-word-particle-highlights.md is excluded by !changes/**
  • changes/339-hyprland-overlay-pin.md is excluded by !changes/**
  • changes/340-default-subtitle-keybindings.md is excluded by !changes/**
  • docs-site/configuration.md is excluded by !docs-site/**
  • docs-site/public/config.example.jsonc is excluded by !docs-site/**
  • docs-site/shortcuts.md is excluded by !docs-site/**
  • docs-site/usage.md is excluded by !docs-site/**
📒 Files selected for processing (30)
  • config.example.jsonc
  • package.json
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • src/config/definitions/defaults-core.ts
  • src/config/definitions/domain-registry.test.ts
  • src/core/services/anilist/anilist-token-store.test.ts
  • src/core/services/anilist/anilist-token-store.ts
  • src/core/services/hyprland-window-placement.test.ts
  • src/core/services/hyprland-window-placement.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-manager.test.ts
  • src/core/services/overlay-window-config.test.ts
  • src/core/services/overlay-window-options.ts
  • src/core/services/overlay-window.ts
  • src/core/services/session-bindings.test.ts
  • src/core/services/stats-window-runtime.ts
  • src/core/services/stats-window.test.ts
  • src/core/services/stats-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/renderer/overlay-legacy-cleanup.test.ts
  • src/renderer/style.css
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
  • stats/src/App.tsx
  • stats/src/styles/globals.css
  • stats/src/styles/globals.test.ts
💤 Files with no reviewable changes (1)
  • src/core/services/anilist/anilist-token-store.ts
✅ Files skipped from review due to trivial changes (7)
  • src/core/services/overlay-window-config.test.ts
  • stats/src/styles/globals.test.ts
  • stats/src/App.tsx
  • src/config/definitions/domain-registry.test.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/core/services/overlay-manager.test.ts
  • src/window-trackers/hyprland-tracker.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • src/renderer/style.css

Comment on lines +135 to +151
const run = options.execFileSync ?? execFileSync;
try {
const clients = parseHyprlandClients(
String(run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })),
);
const client = findHyprlandWindowForPlacement(clients, {
pid: options.pid ?? process.pid,
title: options.title,
});
if (!client) {
return false;
}

const dispatches = buildHyprlandPlacementDispatches(client, options.bounds);
for (const args of dispatches) {
run('hyprctl', args, { stdio: 'ignore' });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== hyprctl / execFileSync usage =="
rg -n --type=ts -C2 "hyprctl|execFileSync|ensureHyprlandWindowFloatingByTitle"

echo
echo "== Direct run(...) command option blocks in hyprland-window-placement.ts =="
rg -n --type=ts -C3 "run\\('hyprctl'" src/core/services/hyprland-window-placement.ts

Repository: ksyasuda/SubMiner

Length of output: 12538


🏁 Script executed:

#!/bin/bash

# Find all direct hyprctl invocations (execSync and execFileSync)
echo "=== All hyprctl calls with context ==="
rg -n "execSync|execFileSync" -A5 -B2 src/ --type=ts | grep -A5 -B2 "hyprctl"

echo ""
echo "=== Check Node.js version requirement ==="
if [ -f "package.json" ]; then
  jq '.engines.node // .engines // "not specified"' package.json
fi

echo ""
echo "=== Timeout option in tests ==="
rg -n "timeout" src/core/services/hyprland-window-placement.test.ts --type=ts

Repository: ksyasuda/SubMiner

Length of output: 1908


🏁 Script executed:

#!/bin/bash

# Check if there are existing examples of timeout usage in the codebase
echo "=== Existing timeout patterns in execFileSync/execSync ==="
rg -n "timeout\s*:" --type=ts | head -20

echo ""
echo "=== Check hyprland-tracker.ts for all hyprctl calls ==="
rg -n "hyprctl" src/window-trackers/hyprland-tracker.ts -A3 -B3 --type=ts

Repository: ksyasuda/SubMiner

Length of output: 2576


🌐 Web query:

Node.js execFileSync timeout option support version requirement

💡 Result:

The timeout option for child_process.execFileSync has been supported since Node.js v0.11.12, the version when execFileSync was added. This option is inherited from child_process.spawnSync, which also gained timeout support at the same time (v0.11.12). Official Node.js documentation for versions v16.x, v18.x, v20.x, v22.x, and v25.x all list timeout as an available option with default undefined, confirming long-standing support. A 2015 documentation commit references timeout behavior, and third-party sources like execa note it as a "long time" feature for sync methods predating async spawn support (added in v14+).

Citations:


Add timeouts to blocking hyprctl calls to prevent main-thread stalls.

Lines 138 and 150 in src/core/services/hyprland-window-placement.ts execute synchronous external commands without timeout protection. If hyprctl hangs, the Electron main thread will block indefinitely, freezing window operations. The timeout option is available on execFileSync and is already used elsewhere in the codebase (e.g., win32.ts:199).

Proposed fix
+const HYPRCTL_TIMEOUT_MS = 750;
+
 export function ensureHyprlandWindowFloatingByTitle(options: {
@@
   const run = options.execFileSync ?? execFileSync;
   try {
     const clients = parseHyprlandClients(
-      String(run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })),
+      String(
+        run('hyprctl', ['-j', 'clients'], {
+          encoding: 'utf-8',
+          timeout: HYPRCTL_TIMEOUT_MS,
+          killSignal: 'SIGKILL',
+        }),
+      ),
     );
@@
     const dispatches = buildHyprlandPlacementDispatches(client, options.bounds);
     for (const args of dispatches) {
-      run('hyprctl', args, { stdio: 'ignore' });
+      run('hyprctl', args, {
+        stdio: 'ignore',
+        timeout: HYPRCTL_TIMEOUT_MS,
+        killSignal: 'SIGKILL',
+      });
     }

Note: Similar uncovered calls exist in src/window-trackers/hyprland-tracker.ts (lines 322 and 356) that should receive the same treatment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/hyprland-window-placement.ts` around lines 135 - 151, The
synchronous hyprctl calls use the local alias run (set from options.execFileSync
?? execFileSync) without a timeout, so wrap both invocations—when fetching
clients (run('hyprctl', ['-j', 'clients'], { encoding: 'utf-8' })) and when
dispatching placement args (run('hyprctl', args, { stdio: 'ignore' }))—to
include a timeout option (e.g., timeout: <ms>) to avoid blocking the Electron
main thread; add a clear constant name (e.g., HYPRCTL_TIMEOUT_MS) if one doesn’t
exist and pass it into both run calls, preserving any existing options
(encoding/stdio) while merging the timeout, and apply the same pattern in
findHyprlandWindowForPlacement call sites and the similar calls in
src/window-trackers/hyprland-tracker.ts.

Comment thread src/core/services/tokenizer/annotation-stage.ts
ksyasuda added 4 commits May 4, 2026 00:06
- 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)
ksyasuda added 26 commits May 4, 2026 00:06
- 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
- 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
- Hide annotation metadata for auxiliary inflection and ja-nai endings
- Preserve lexical `くれる` forms and add regression coverage
- Use a pure-kana fixture for the subtitle token N+1 case
- Update task notes for the latest CodeRabbit follow-up
- Ignore background stats daemons during regular app startup
- Split standalone grammar endings before applying annotations
- Clear helper-span annotations for auxiliary-only tokens
- 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
- 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
- keep visible overlays alive during transient macOS tracker loss
- reuse the running mpv overlay path on playlist navigation
- update regression coverage and changelog fragments
- 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
- 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
- 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
- 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
@ksyasuda ksyasuda force-pushed the tokenizer-updates branch from cea3c20 to 4d5bf3d Compare May 4, 2026 07:06
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
src/renderer/subtitle-render.test.ts (1)

922-927: ⚡ Quick win

Make the JLPT-only check tolerant of CSS rule grouping.

Requiring an empty extracted block is brittle; if this selector is later grouped into a shared rule, the test will false-fail even when no color override is introduced. Checking for the absence of text-color declarations is more stable.

Suggested test tweak
-    assert.equal(
-      jlptOnlyColorBlock,
-      '',
-      `word-jlpt-n${level} (without other annotations) must not set text color — JLPT only paints the underline`,
-    );
+    assert.doesNotMatch(
+      jlptOnlyColorBlock,
+      /(?:^|\n)\s*(?:color|-webkit-text-fill-color)\s*:/m,
+      `word-jlpt-n${level} (without other annotations) must not set text color — JLPT only paints the underline`,
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/subtitle-render.test.ts` around lines 922 - 927, The test
currently asserts jlptOnlyColorBlock === '' which breaks if the selector is
grouped; instead assert that the extracted block (from
extractClassBlock(cssText, buildJlptColorSelector(level))) does not contain any
text color declarations: check jlptOnlyColorBlock for absence of patterns like
"color\s*:" (and optionally "fill\s*:" if SVG/text-fill is relevant) using a
regex-based negative assertion so grouping won't cause false failures; update
the assertion to use a doesNotMatch/doesNotContain-style assertion against that
regex and keep the rest of the test intact.
src/core/services/overlay-window.ts (1)

1-2: Align with codebase import style for consistency.

The default import import electron from 'electron' is safe in this project (esModuleInterop is enabled), but the majority of the codebase uses named imports instead. Switch to named imports to match the established pattern:

import { BrowserWindow, screen } from 'electron';
import type { Session } from 'electron';

Also applies to: 19-19

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/overlay-window.ts` around lines 1 - 2, Replace the default
import from 'electron' with named imports to match project style: import
BrowserWindow and screen as regular imports and keep Session as a type import;
update any other default-import occurrences (the second instance noted) to the
same pattern so all electron imports in this module use named imports instead of
the default electron export.
src/main.ts (2)

4269-4271: ⚡ Quick win

Deduplicate Anki callback wiring to avoid drift.

setKnownWordCacheUpdatedCallback is registered in two places with the same function. Extracting a single helper for Anki callback registration will reduce maintenance risk when callback setup changes.

♻️ Suggested refactor
+function attachAnkiIntegrationCallbacks(integration: AnkiIntegration | null): void {
+  integration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
+  integration?.setKnownWordCacheUpdatedCallback(refreshCurrentSubtitleAfterKnownWordUpdate);
+}
+
 function initializeOverlayRuntime(): void {
   initializeOverlayRuntimeHandler();
-  appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
-  appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(
-    refreshCurrentSubtitleAfterKnownWordUpdate,
-  );
+  attachAnkiIntegrationCallbacks(appState.ankiIntegration);
   syncOverlayMpvSubtitleSuppression();
 }
@@
       setAnkiIntegration: (integration: AnkiIntegration | null) => {
         appState.ankiIntegration = integration;
-        appState.ankiIntegration?.setRecordCardsMinedCallback(recordTrackedCardsMined);
-        appState.ankiIntegration?.setKnownWordCacheUpdatedCallback(
-          refreshCurrentSubtitleAfterKnownWordUpdate,
-        );
+        attachAnkiIntegrationCallbacks(appState.ankiIntegration);
       },

Also applies to: 4979-4981

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 4269 - 4271, Extract a small helper (e.g.,
registerAnkiKnownWordCallback) that takes appState (or appState.ankiIntegration)
and wires
appState.ankiIntegration.setKnownWordCacheUpdatedCallback(refreshCurrentSubtitleAfterKnownWordUpdate);
then replace both inline registrations with calls to this helper; reference the
existing symbols appState.ankiIntegration, setKnownWordCacheUpdatedCallback, and
refreshCurrentSubtitleAfterKnownWordUpdate so the single helper is used wherever
the callback was previously set.

1410-1424: 🏗️ Heavy lift

Consider extracting Linux fullscreen-refresh orchestration out of src/main.ts.

The new scheduling/cancellation flow is cohesive, but it adds more responsibility to an already very large entrypoint. Moving this orchestration behind a focused runtime module would better preserve maintainability.

As per coding guidelines, "**/*.{ts,tsx}: TypeScript files should be kept small and follow existing patterns".

Also applies to: 3862-3872, 5480-5511

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main.ts` around lines 1410 - 1424, The new scheduling/cancellation logic
(symbols: cancelLinuxMpvFullscreenOverlayRefreshBurst,
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst,
clearScheduledSubtitlePrefetchRefresh, subtitlePrefetchRefreshTimer,
SEEK_THRESHOLD_SECONDS) should be extracted from src/main.ts into a focused
runtime module: create a new module (e.g., linuxFullscreenRefresh) that owns the
timers, the CancelLinuxMpvFullscreenOverlayRefreshBurst type and the three
functions, move the implementation there, export the public API (start/schedule,
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst,
clearScheduledSubtitlePrefetchRefresh, and SEEK_THRESHOLD_SECONDS if widely
used), and update main.ts to import and call those exports; ensure no runtime
behavior changes and update any other places referenced in the review (lines
~3862-3872, ~5480-5511) to use the new module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/services/overlay-window.ts`:
- Around line 56-59: The call to ensureHyprlandWindowFloatingByTitle is in the
hot geometry update path (after normalizeOverlayWindowBoundsForPlatform ->
window.setBounds) and makes synchronous hyprctl calls that can block the main
thread; to fix this, remove direct calls from that hot path and instead debounce
or defer them: e.g., replace the immediate ensureHyprlandWindowFloatingByTitle({
title: window.getTitle(), bounds }) with a debounced / scheduled invocation
(triggered only after a short idle period or on explicit rebind/layer
transitions), or move the call into the rebind/layer-change handler so
normalizeOverlayWindowBoundsForPlatform and window.setBounds remain fast and
non-blocking. Ensure the debounced function still uses window.getTitle() and the
computed bounds when invoked.

In `@src/renderer/subtitle-render.test.ts`:
- Around line 1105-1123: The test loop that asserts JLPT underline color locking
misses the 'word-name-match' annotation, so add 'word-name-match' to the array
of annotation classes checked in the for loop (the block that iterates over
['word-known', 'word-n-plus-one', ...]) so extractClassBlock(cssText,
`#subtitleRoot .word.word-jlpt-n${level}.${annotationClass}`) is also validated
for 'word-name-match' and the assert.match continues to verify the
text-decoration-color rule for that class.

---

Nitpick comments:
In `@src/core/services/overlay-window.ts`:
- Around line 1-2: Replace the default import from 'electron' with named imports
to match project style: import BrowserWindow and screen as regular imports and
keep Session as a type import; update any other default-import occurrences (the
second instance noted) to the same pattern so all electron imports in this
module use named imports instead of the default electron export.

In `@src/main.ts`:
- Around line 4269-4271: Extract a small helper (e.g.,
registerAnkiKnownWordCallback) that takes appState (or appState.ankiIntegration)
and wires
appState.ankiIntegration.setKnownWordCacheUpdatedCallback(refreshCurrentSubtitleAfterKnownWordUpdate);
then replace both inline registrations with calls to this helper; reference the
existing symbols appState.ankiIntegration, setKnownWordCacheUpdatedCallback, and
refreshCurrentSubtitleAfterKnownWordUpdate so the single helper is used wherever
the callback was previously set.
- Around line 1410-1424: The new scheduling/cancellation logic (symbols:
cancelLinuxMpvFullscreenOverlayRefreshBurst,
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst,
clearScheduledSubtitlePrefetchRefresh, subtitlePrefetchRefreshTimer,
SEEK_THRESHOLD_SECONDS) should be extracted from src/main.ts into a focused
runtime module: create a new module (e.g., linuxFullscreenRefresh) that owns the
timers, the CancelLinuxMpvFullscreenOverlayRefreshBurst type and the three
functions, move the implementation there, export the public API (start/schedule,
cancelPendingLinuxMpvFullscreenOverlayRefreshBurst,
clearScheduledSubtitlePrefetchRefresh, and SEEK_THRESHOLD_SECONDS if widely
used), and update main.ts to import and call those exports; ensure no runtime
behavior changes and update any other places referenced in the review (lines
~3862-3872, ~5480-5511) to use the new module.

In `@src/renderer/subtitle-render.test.ts`:
- Around line 922-927: The test currently asserts jlptOnlyColorBlock === ''
which breaks if the selector is grouped; instead assert that the extracted block
(from extractClassBlock(cssText, buildJlptColorSelector(level))) does not
contain any text color declarations: check jlptOnlyColorBlock for absence of
patterns like "color\s*:" (and optionally "fill\s*:" if SVG/text-fill is
relevant) using a regex-based negative assertion so grouping won't cause false
failures; update the assertion to use a doesNotMatch/doesNotContain-style
assertion against that regex and keep the rest of the test intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ebb685a1-7fff-458b-bb15-473526b4592a

📥 Commits

Reviewing files that changed from the base of the PR and between cea3c20 and 4d5bf3d.

⛔ Files ignored due to path filters (68)
  • backlog/completed/task-336 - Fix-Hyprland-fullscreen-overlay-downward-offset.md is excluded by !backlog/**
  • backlog/completed/task-339 - Stop-pinning-Hyprland-overlay-windows.md is excluded by !backlog/**
  • backlog/tasks/task-304 - Fix-N1-sentence-boundary-counting-across-Yomitan-punctuation-gaps.md is excluded by !backlog/**
  • backlog/tasks/task-305 - Use-Yomitan-word-classes-for-subtitle-token-POS-filtering.md is excluded by !backlog/**
  • backlog/tasks/task-306 - Fix-Hyprland-fullscreen-overlay-geometry-and-hover-pause.md is excluded by !backlog/**
  • backlog/tasks/task-307 - Exclude-kana-only-words-from-N1-subtitle-targets.md is excluded by !backlog/**
  • backlog/tasks/task-308 - Restore-persistent-JLPT-subtitle-underlines.md is excluded by !backlog/**
  • backlog/tasks/task-309 - Accept-modified-follow-up-digits-for-multi-line-sentence-mining.md is excluded by !backlog/**
  • backlog/tasks/task-310 - Suppress-N1-highlight-for-kana-only-candidate-sentences.md is excluded by !backlog/**
  • backlog/tasks/task-311 - Suppress-auxiliary-inflection-fragments-from-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-312 - Suppress-ja-nai-explanatory-ending-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-315 - Suppress-annotations-for-standalone-じゃない-and-です-ending-tokens.md is excluded by !backlog/**
  • backlog/tasks/task-316 - Fix-macOS-launcher-playback-exit-with-background-stats-daemon.md is excluded by !backlog/**
  • backlog/tasks/task-318 - Keep-JLPT-underline-color-fixed-after-lookup-selection.md is excluded by !backlog/**
  • backlog/tasks/task-319 - Suppress-annotations-for-expressive-interjection-subtitles.md is excluded by !backlog/**
  • backlog/tasks/task-320 - Refresh-current-subtitle-known-word-highlight-after-successful-mining.md is excluded by !backlog/**
  • backlog/tasks/task-321 - Preserve-word-audio-during-manual-clipboard-card-updates.md is excluded by !backlog/**
  • backlog/tasks/task-322 - Fix-failing-CI-checks-on-PR-57.md is excluded by !backlog/**
  • backlog/tasks/task-323 - Fix-macOS-overlay-hiding-while-mpv-remains-active.md is excluded by !backlog/**
  • backlog/tasks/task-324 - Fix-mpv-playlist-changes-re-running-app-warmups.md is excluded by !backlog/**
  • backlog/tasks/task-325 - Keep-JLPT-underline-color-fixed-with-combined-lookup-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-326 - Fix-AniList-post-watch-update-after-skipped-completion-threshold.md is excluded by !backlog/**
  • backlog/tasks/task-327 - Restore-stats-daemon-deferral-when-launching-playback.md is excluded by !backlog/**
  • backlog/tasks/task-328 - Keep-subtitle-prefetch-running-after-immediate-cached-annotation-render.md is excluded by !backlog/**
  • backlog/tasks/task-331 - Address-unresolved-CodeRabbit-comments-on-PR-57.md is excluded by !backlog/**
  • backlog/tasks/task-332 - Fix-subtitle-frequency-annotation-missing-ranks-shown-in-Yomitan-popup.md is excluded by !backlog/**
  • backlog/tasks/task-333 - Suppress-aru-subtitle-annotations.md is excluded by !backlog/**
  • backlog/tasks/task-334 - Assess-and-address-PR-57-latest-CodeRabbit-comments.md is excluded by !backlog/**
  • backlog/tasks/task-335 - Fix-Linux-AniList-setup-gate-using-stored-keyring-token.md is excluded by !backlog/**
  • backlog/tasks/task-337 - Fix-transient-Linux-safeStorage-failure-poisoning-AniList-token-store.md is excluded by !backlog/**
  • backlog/tasks/task-338 - Fix-known-word-highlight-on-standalone-subtitle-particles.md is excluded by !backlog/**
  • backlog/tasks/task-340 - Restore-default-replay-and-next-subtitle-overlay-keybindings.md is excluded by !backlog/**
  • changes/305-tokenizer-word-class-pos-filtering.md is excluded by !changes/**
  • changes/306-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/307-kana-nplusone-targets.md is excluded by !changes/**
  • changes/308-jlpt-underlines.md is excluded by !changes/**
  • changes/309-multi-mine-modified-digits.md is excluded by !changes/**
  • changes/311-auxiliary-inflection-annotation-filter.md is excluded by !changes/**
  • changes/312-grammar-ending-annotation-filter.md is excluded by !changes/**
  • changes/316-macos-playback-stats-daemon.md is excluded by !changes/**
  • changes/318-jlpt-underline-selection.md is excluded by !changes/**
  • changes/319-interjection-annotation-filter.md is excluded by !changes/**
  • changes/320-current-subtitle-known-highlight.md is excluded by !changes/**
  • changes/321-grammar-ending-pattern-filter.md is excluded by !changes/**
  • changes/322-preserve-word-audio-manual-update.md is excluded by !changes/**
  • changes/323-macos-overlay-tracker-flaps.md is excluded by !changes/**
  • changes/324-mpv-playlist-overlay-reuse.md is excluded by !changes/**
  • changes/325-jlpt-combined-annotation-underlines.md is excluded by !changes/**
  • changes/326-anilist-time-position-post-watch.md is excluded by !changes/**
  • changes/327-stats-daemon-deferral.md is excluded by !changes/**
  • changes/328-subtitle-prefetch-cache-hit-resume.md is excluded by !changes/**
  • changes/332-subtitle-frequency-ordinal-prefix.md is excluded by !changes/**
  • changes/333-aru-annotation-filter.md is excluded by !changes/**
  • changes/334-coderabbit-followups.md is excluded by !changes/**
  • changes/335-anilist-linux-token-setup.md is excluded by !changes/**
  • changes/336-hyprland-fullscreen-overlay.md is excluded by !changes/**
  • changes/336-overlay-focus-ring.md is excluded by !changes/**
  • changes/337-anilist-safe-storage-retry.md is excluded by !changes/**
  • changes/338-known-word-particle-highlights.md is excluded by !changes/**
  • changes/339-hyprland-overlay-pin.md is excluded by !changes/**
  • changes/340-default-subtitle-keybindings.md is excluded by !changes/**
  • docs-site/anki-integration.md is excluded by !docs-site/**
  • docs-site/configuration.md is excluded by !docs-site/**
  • docs-site/mining-workflow.md is excluded by !docs-site/**
  • docs-site/public/config.example.jsonc is excluded by !docs-site/**
  • docs-site/shortcuts.md is excluded by !docs-site/**
  • docs-site/troubleshooting.md is excluded by !docs-site/**
  • docs-site/usage.md is excluded by !docs-site/**
📒 Files selected for processing (91)
  • config.example.jsonc
  • package.json
  • plugin/subminer/lifecycle.lua
  • plugin/subminer/process.lua
  • plugin/subminer/session_bindings.lua
  • scripts/test-plugin-session-bindings.lua
  • scripts/test-plugin-start-gate.lua
  • src/anki-integration.test.ts
  • src/anki-integration.ts
  • src/anki-integration/card-creation-manual-update.test.ts
  • src/anki-integration/card-creation.ts
  • src/anki-integration/known-word-cache.test.ts
  • src/anki-integration/known-word-cache.ts
  • src/config/definitions/defaults-core.ts
  • src/config/definitions/domain-registry.test.ts
  • src/core/services/anilist/anilist-token-store.test.ts
  • src/core/services/anilist/anilist-token-store.ts
  • src/core/services/anilist/anilist-updater.test.ts
  • src/core/services/anilist/anilist-updater.ts
  • src/core/services/hyprland-window-placement.test.ts
  • src/core/services/hyprland-window-placement.ts
  • src/core/services/ipc.test.ts
  • src/core/services/ipc.ts
  • src/core/services/mpv-properties.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/mpv-protocol.ts
  • src/core/services/mpv.test.ts
  • src/core/services/mpv.ts
  • src/core/services/overlay-manager.test.ts
  • src/core/services/overlay-visibility.test.ts
  • src/core/services/overlay-visibility.ts
  • src/core/services/overlay-window-config.test.ts
  • src/core/services/overlay-window-options.ts
  • src/core/services/overlay-window.ts
  • src/core/services/session-bindings.test.ts
  • src/core/services/stats-window-runtime.ts
  • src/core/services/stats-window.test.ts
  • src/core/services/stats-window.ts
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer.ts
  • src/core/services/tokenizer/annotation-stage.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/core/services/tokenizer/grammar-ending.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/tokenizer/parser-selection-stage.test.ts
  • src/core/services/tokenizer/parser-selection-stage.ts
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
  • src/main.ts
  • src/main/dependencies.ts
  • src/main/runtime/anilist-post-watch.test.ts
  • src/main/runtime/anilist-post-watch.ts
  • src/main/runtime/anilist-setup-protocol-main-deps.test.ts
  • src/main/runtime/anilist-setup-protocol.test.ts
  • src/main/runtime/anilist-setup-protocol.ts
  • src/main/runtime/anilist-setup.test.ts
  • src/main/runtime/anilist-setup.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/main/runtime/app-lifecycle-actions.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/composers/anilist-setup-composer.test.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/main/runtime/mpv-main-event-actions.test.ts
  • src/main/runtime/mpv-main-event-actions.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/startup-config.test.ts
  • src/main/runtime/startup-config.ts
  • src/main/runtime/stats-server-routing.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/renderer/handlers/mouse.test.ts
  • src/renderer/handlers/mouse.ts
  • src/renderer/overlay-legacy-cleanup.test.ts
  • src/renderer/style.css
  • src/renderer/subtitle-render.test.ts
  • src/token-merger.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/window-trackers/hyprland-tracker.ts
  • stats/src/App.tsx
  • stats/src/styles/globals.css
  • stats/src/styles/globals.test.ts
💤 Files with no reviewable changes (1)
  • src/core/services/anilist/anilist-token-store.ts
✅ Files skipped from review due to trivial changes (38)
  • stats/src/styles/globals.test.ts
  • src/main/runtime/composers/anilist-setup-composer.test.ts
  • src/main/runtime/stats-server-routing.test.ts
  • stats/src/App.tsx
  • src/main/runtime/mpv-client-event-bindings.ts
  • src/core/services/stats-window-runtime.ts
  • src/main/runtime/anilist-setup-protocol-main-deps.test.ts
  • src/core/services/mpv-protocol.test.ts
  • src/core/services/tokenizer/parser-enrichment-stage.test.ts
  • src/main/runtime/stats-server-routing.ts
  • src/window-trackers/hyprland-tracker.test.ts
  • src/renderer/handlers/keyboard.test.ts
  • src/main/runtime/mpv-main-event-bindings.ts
  • src/main/runtime/app-lifecycle-main-cleanup.test.ts
  • src/core/services/stats-window.test.ts
  • src/config/definitions/domain-registry.test.ts
  • src/core/services/session-bindings.test.ts
  • src/renderer/handlers/keyboard.ts
  • src/main/runtime/anilist-setup.test.ts
  • src/anki-integration.ts
  • src/anki-integration.test.ts
  • src/core/services/anilist/anilist-updater.test.ts
  • src/main/runtime/mpv-client-event-bindings.test.ts
  • src/main/runtime/composers/startup-lifecycle-composer.test.ts
  • src/core/services/mpv.ts
  • src/core/services/stats-window.ts
  • src/core/services/overlay-manager.test.ts
  • config.example.jsonc
  • src/main/runtime/mpv-main-event-actions.test.ts
  • src/core/services/tokenizer.ts
  • src/main/runtime/mpv-main-event-main-deps.test.ts
  • src/core/services/tokenizer/yomitan-parser-runtime.test.ts
  • src/main/runtime/startup-config.test.ts
  • src/window-trackers/hyprland-tracker.ts
  • src/core/services/tokenizer/grammar-ending.ts
  • src/core/services/ipc.test.ts
  • stats/src/styles/globals.css
  • src/core/services/tokenizer/yomitan-parser-runtime.ts
🚧 Files skipped from review as they are similar to previous changes (35)
  • src/core/services/overlay-window-config.test.ts
  • src/core/services/overlay-window-options.ts
  • plugin/subminer/lifecycle.lua
  • src/main/runtime/app-lifecycle-actions.ts
  • src/core/services/anilist/anilist-updater.ts
  • src/core/services/ipc.ts
  • src/main/runtime/app-lifecycle-main-cleanup.ts
  • src/main/runtime/startup-config.ts
  • src/renderer/overlay-legacy-cleanup.test.ts
  • src/core/services/mpv-protocol.ts
  • src/main/runtime/mpv-main-event-actions.ts
  • src/main/runtime/mpv-main-event-main-deps.ts
  • src/main/runtime/anilist-setup-protocol.test.ts
  • src/main/runtime/anilist-setup.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.test.ts
  • src/renderer/handlers/mouse.ts
  • src/core/services/hyprland-window-placement.ts
  • src/core/services/tokenizer/parser-enrichment-stage.ts
  • src/core/services/hyprland-window-placement.test.ts
  • src/main/runtime/anilist-setup-protocol.ts
  • src/main/runtime/linux-mpv-fullscreen-overlay-refresh.ts
  • src/core/services/tokenizer/annotation-stage.ts
  • src/config/definitions/defaults-core.ts
  • scripts/test-plugin-session-bindings.lua
  • src/anki-integration/known-word-cache.test.ts
  • src/token-merger.ts
  • src/main/runtime/app-lifecycle-actions.test.ts
  • src/anki-integration/card-creation.ts
  • src/main/runtime/anilist-post-watch.test.ts
  • package.json
  • plugin/subminer/process.lua
  • src/renderer/style.css
  • src/core/services/tokenizer/subtitle-annotation-filter.ts
  • src/renderer/handlers/mouse.test.ts
  • src/core/services/tokenizer/annotation-stage.test.ts

Comment on lines +56 to 59
const bounds = normalizeOverlayWindowBoundsForPlatform(geometry, process.platform, screen);
window.setBounds(bounds);
ensureHyprlandWindowFloatingByTitle({ title: window.getTitle(), bounds });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid synchronous Hyprland placement calls on every bounds update.

Line 58 pulls ensureHyprlandWindowFloatingByTitle into the geometry hot path; that helper performs synchronous hyprctl calls, which can block the Electron main thread during frequent resize/move updates. Please debounce/collapse this path or trigger it only on explicit rebind/layer transitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/services/overlay-window.ts` around lines 56 - 59, The call to
ensureHyprlandWindowFloatingByTitle is in the hot geometry update path (after
normalizeOverlayWindowBoundsForPlatform -> window.setBounds) and makes
synchronous hyprctl calls that can block the main thread; to fix this, remove
direct calls from that hot path and instead debounce or defer them: e.g.,
replace the immediate ensureHyprlandWindowFloatingByTitle({ title:
window.getTitle(), bounds }) with a debounced / scheduled invocation (triggered
only after a short idle period or on explicit rebind/layer transitions), or move
the call into the rebind/layer-change handler so
normalizeOverlayWindowBoundsForPlatform and window.setBounds remain fast and
non-blocking. Ensure the debounced function still uses window.getTitle() and the
computed bounds when invoked.

Comment on lines +1105 to +1123
for (const annotationClass of [
'word-known',
'word-n-plus-one',
'word-frequency-single',
'word-frequency-band-2',
]) {
const combinedAnnotationBlock = extractClassBlock(
cssText,
`#subtitleRoot .word.word-jlpt-n${level}.${annotationClass}`,
);
assert.match(
combinedAnnotationBlock,
new RegExp(
`text-decoration-color:\\s*var\\(--subtitle-jlpt-n${level}-color,\\s*#[0-9a-f]{6}\\)\\s*!important;`,
'i',
),
`combined JLPT ${annotationClass} selector should lock underline color`,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add word-name-match to the JLPT lock coverage.

This loop covers known, N+1, and frequency tokens, but it skips the name-match path, which is also higher priority than JLPT styling. A regression in that combination would slip through.

Suggested coverage addition
     for (const annotationClass of [
       'word-known',
       'word-n-plus-one',
+      'word-name-match',
       'word-frequency-single',
       'word-frequency-band-2',
     ]) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const annotationClass of [
'word-known',
'word-n-plus-one',
'word-frequency-single',
'word-frequency-band-2',
]) {
const combinedAnnotationBlock = extractClassBlock(
cssText,
`#subtitleRoot .word.word-jlpt-n${level}.${annotationClass}`,
);
assert.match(
combinedAnnotationBlock,
new RegExp(
`text-decoration-color:\\s*var\\(--subtitle-jlpt-n${level}-color,\\s*#[0-9a-f]{6}\\)\\s*!important;`,
'i',
),
`combined JLPT ${annotationClass} selector should lock underline color`,
);
}
for (const annotationClass of [
'word-known',
'word-n-plus-one',
'word-name-match',
'word-frequency-single',
'word-frequency-band-2',
]) {
const combinedAnnotationBlock = extractClassBlock(
cssText,
`#subtitleRoot .word.word-jlpt-n${level}.${annotationClass}`,
);
assert.match(
combinedAnnotationBlock,
new RegExp(
`text-decoration-color:\\s*var\\(--subtitle-jlpt-n${level}-color,\\s*#[0-9a-f]{6}\\)\\s*!important;`,
'i',
),
`combined JLPT ${annotationClass} selector should lock underline color`,
);
}
🧰 Tools
🪛 ast-grep (0.42.1)

[warning] 1116-1119: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
text-decoration-color:\\s*var\\(--subtitle-jlpt-n${level}-color,\\s*#[0-9a-f]{6}\\)\\s*!important;,
'i',
)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/subtitle-render.test.ts` around lines 1105 - 1123, The test loop
that asserts JLPT underline color locking misses the 'word-name-match'
annotation, so add 'word-name-match' to the array of annotation classes checked
in the for loop (the block that iterates over ['word-known', 'word-n-plus-one',
...]) so extractClassBlock(cssText, `#subtitleRoot
.word.word-jlpt-n${level}.${annotationClass}`) is also validated for
'word-name-match' and the assert.match continues to verify the
text-decoration-color rule for that class.

- 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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/core/services/tokenizer.test.ts (1)

4233-5067: 🏗️ Heavy lift

Consider table-driving or splitting these exclusion-rule cases.

This block adds many near-identical fixtures and token-summary assertions that mostly vary by inputs and expected cleared fields. A small shared helper plus a case matrix, or a dedicated subtitle-annotation test file, would make future rule changes much easier to maintain in this already very large TypeScript test file.

As per coding guidelines, **/*.{ts,tsx}: TypeScript files should be kept small and follow existing patterns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/services/tokenizer.test.ts` around lines 4233 - 5067, The tests
repeat the same pattern around tokenizeSubtitle and makeDepsFromYomitanTokens
with only inputs and expected cleared fields changing; refactor into a
table-driven helper to reduce duplication: create a shared helper (e.g.,
runAnnotationClearCase) that calls tokenizeSubtitle with a provided token
fixture from makeDepsFromYomitanTokens, computes tokenSummary, and asserts
expected entries, then replace each duplicated test with a lightweight row in a
case matrix (surface/input, mecab tokens, deps overrides, expectations).
Optionally move these cases into a new test module to keep the original file
small; ensure helper references include tokenizeSubtitle,
makeDepsFromYomitanTokens and tokenSummary to locate where to update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/services/tokenizer.test.ts`:
- Around line 3233-3285: The test incorrectly asserts that the kanji token '点'
should have isKnown cleared; update the assertion in the tokenizeSubtitle test
so the excluded-kanji case preserves isKnown for the '点' token (change the
expectation on result.tokens?.[1]?.isKnown to true), and ensure any related
assertions about frequencyRank and jlptLevel remain consistent with the
preserved-known behavior; locate this check in the test around the
tokenizeSubtitle invocation and the assertions for result.tokens?.[1].

---

Nitpick comments:
In `@src/core/services/tokenizer.test.ts`:
- Around line 4233-5067: The tests repeat the same pattern around
tokenizeSubtitle and makeDepsFromYomitanTokens with only inputs and expected
cleared fields changing; refactor into a table-driven helper to reduce
duplication: create a shared helper (e.g., runAnnotationClearCase) that calls
tokenizeSubtitle with a provided token fixture from makeDepsFromYomitanTokens,
computes tokenSummary, and asserts expected entries, then replace each
duplicated test with a lightweight row in a case matrix (surface/input, mecab
tokens, deps overrides, expectations). Optionally move these cases into a new
test module to keep the original file small; ensure helper references include
tokenizeSubtitle, makeDepsFromYomitanTokens and tokenSummary to locate where to
update.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7ba20e81-5736-48b9-a4f4-0095fce55be6

📥 Commits

Reviewing files that changed from the base of the PR and between 4d5bf3d and 4cb7b53.

⛔ Files ignored due to path filters (1)
  • backlog/tasks/task-341 - Fix-frequency-highlight-for-honorific-prefix-noun-tokens.md is excluded by !backlog/**
📒 Files selected for processing (2)
  • src/core/services/tokenizer.test.ts
  • src/core/services/tokenizer/annotation-stage.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/core/services/tokenizer/annotation-stage.ts

Comment on lines +3233 to +3285
test('tokenizeSubtitle clears known-word highlight for exact non-independent kanji noun tokens', async () => {
const result = await tokenizeSubtitle(
'その点',
makeDepsFromYomitanTokens(
[
{ surface: 'その', reading: 'その', headword: 'その' },
{ surface: '点', reading: 'てん', headword: '点' },
],
{
isKnownWord: (text) => text === '点' || text === 'てん',
getFrequencyDictionaryEnabled: () => true,
getFrequencyRank: (text) => (text === '点' ? 1384 : null),
getJlptLevel: (text) => (text === '点' ? 'N3' : null),
tokenizeWithMecab: async () => [
{
headword: 'その',
surface: 'その',
reading: 'ソノ',
startPos: 0,
endPos: 2,
partOfSpeech: PartOfSpeech.other,
pos1: '連体詞',
isMerged: false,
isKnown: false,
isNPlusOneTarget: false,
},
{
headword: '点',
surface: '点',
reading: 'テン',
startPos: 2,
endPos: 3,
partOfSpeech: PartOfSpeech.noun,
pos1: '名詞',
pos2: '非自立',
pos3: '一般',
isMerged: false,
isKnown: false,
isNPlusOneTarget: false,
},
],
},
),
);

assert.equal(result.tokens?.length, 2);
assert.equal(result.tokens?.[0]?.isKnown, false);
assert.equal(result.tokens?.[1]?.surface, '点');
assert.equal(result.tokens?.[1]?.isKnown, false);
assert.equal(result.tokens?.[1]?.isNPlusOneTarget, false);
assert.equal(result.tokens?.[1]?.frequencyRank, undefined);
assert.equal(result.tokens?.[1]?.jlptLevel, undefined);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve isKnown for excluded kanji token .

Line 3281 expects isKnown to be cleared, but this is the exact excluded-kanji case the PR is supposed to preserve. Keeping this assertion as false would let a regression slip through on the new isKnown retention behavior.

Proposed test fix
-test('tokenizeSubtitle clears known-word highlight for exact non-independent kanji noun tokens', async () => {
+test('tokenizeSubtitle preserves known-word highlight for exact non-independent kanji noun tokens', async () => {
@@
-  assert.equal(result.tokens?.[1]?.isKnown, false);
+  assert.equal(result.tokens?.[1]?.isKnown, true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/services/tokenizer.test.ts` around lines 3233 - 3285, The test
incorrectly asserts that the kanji token '点' should have isKnown cleared; update
the assertion in the tokenizeSubtitle test so the excluded-kanji case preserves
isKnown for the '点' token (change the expectation on result.tokens?.[1]?.isKnown
to true), and ensure any related assertions about frequencyRank and jlptLevel
remain consistent with the preserved-known behavior; locate this check in the
test around the tokenizeSubtitle invocation and the assertions for
result.tokens?.[1].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant