refactor(main): eliminate unsafe runtime cast escapes

Tighten main/runtime dependency contracts to remove non-test `as never` and `as unknown as` usage so type drift surfaces during compile/test checks instead of at runtime.
This commit is contained in:
2026-02-22 13:59:08 -08:00
parent 420b985c7a
commit a6d85def34
38 changed files with 679 additions and 444 deletions

View File

@@ -1,10 +1,11 @@
--- ---
id: TASK-105 id: TASK-105
title: Eliminate unsafe non-test runtime casts in main boundaries title: Eliminate unsafe non-test runtime casts in main boundaries
status: To Do status: Done
assignee: [] assignee:
- opencode-task105-unsafe-casts
created_date: '2026-02-22 07:13' created_date: '2026-02-22 07:13'
updated_date: '2026-02-22 07:13' updated_date: '2026-02-22 21:56'
labels: labels:
- refactor - refactor
- type-safety - type-safety
@@ -36,15 +37,43 @@ Current scan shows repeated casts in dependency builders and runtime adapters, w
## Acceptance Criteria ## Acceptance Criteria
<!-- AC:BEGIN --> <!-- AC:BEGIN -->
- [ ] #1 Non-test `as never` occurrences in `src/main.ts` and `src/main/runtime` are reduced to zero or documented narrow exceptions. - [x] #1 Non-test `as never` occurrences in `src/main.ts` and `src/main/runtime` are reduced to zero or documented narrow exceptions.
- [ ] #2 Runtime dependency builders compile without unsafe production-path cast escapes. - [x] #2 Runtime dependency builders compile without unsafe production-path cast escapes.
- [ ] #3 Contract regressions are caught by compile/test checks rather than runtime behavior. - [x] #3 Contract regressions are caught by compile/test checks rather than runtime behavior.
<!-- AC:END --> <!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
1) Baseline cast inventory in non-test scope (`src/main.ts`, `src/main/runtime/*`) using `rg` and record before-count in notes.
2) Tighten runtime adapter contracts to remove `as never` escapes in `*-main-deps` modules: MPV/Jellyfin defaults, startup config deps, overlay visibility/options deps, field-grouping deps, CLI context deps, subtitle tokenization deps, dictionary runtime deps, tray/app deps, and MPV event bindings.
3) Update `src/main.ts` boundary wiring to match tightened contracts and remove cast escapes in config hot-reload setters, dictionary lookup setters, MPV defaults/OSD wiring, MPV client factory constructor typing, tray creation, and overlay bootstrap callbacks.
4) Re-run cast scan expecting zero non-test unsafe casts (or document narrow exceptions), run required gates (`bun run build`, `bun run test:core:src`), then attach before/after cast report and finalize AC/DoD.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
Executed TASK-105 implementation with runtime contract tightening across `src/main.ts` + `src/main/runtime/*` non-test files. Removed all unsafe `as never` / `as unknown as` casts in target scope by replacing unknown passthrough contracts with typed deps (MPV/Jellyfin runtime types, overlay/bootstrap deps, tokenizer/CLI deps, dictionary lookup function types, tray deps typing).
Cast reduction report: baseline scan count = 42 non-test matches (`rg -n "as\\s+never|as\\s+unknown\\s+as" src/main.ts src/main/runtime --glob '!**/*.test.ts'`), after changes = 0 matches with same command.
Verification: targeted runtime cast-refactor tests passed (`bun test ...` across 19 files, 35 pass / 0 fail). Required core lane passed: `bun run test:core:src` => 236 pass / 6 skip / 0 fail.
Blocker: `bun run build` currently fails on pre-existing unrelated typing issues in `src/anki-integration/note-update-workflow.test.ts` (6 TS errors). No remaining build errors from TASK-105 touched files.
Follow-up verification rerun after concurrent compile-fix pass: `bun run build` now succeeds on current HEAD (tsc + renderer build + packaging copy step). DoD build gate unblocked.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Eliminated unsafe non-test runtime cast escapes in `src/main.ts` and `src/main/runtime/*` by tightening runtime dependency-builder contracts (MPV/Jellyfin, overlay/bootstrap, CLI/tokenizer, dictionary/tray boundaries) and removing `as never` / `as unknown as` patterns in production paths. Captured cast reduction evidence (42 -> 0 matches in scoped scan), validated focused runtime suites (35 pass), and revalidated required gates (`bun run test:core:src`, `bun run build`) on current HEAD.
<!-- SECTION:FINAL_SUMMARY:END -->
## Definition of Done ## Definition of Done
<!-- DOD:BEGIN --> <!-- DOD:BEGIN -->
- [ ] #1 Cast reduction report attached in task notes (before/after counts). - [x] #1 Cast reduction report attached in task notes (before/after counts).
- [ ] #2 `bun run build` and `bun run test:core:src` pass. - [x] #2 `bun run build` and `bun run test:core:src` pass.
- [ ] #3 Any remaining exceptions have explicit rationale in code comments or task notes. - [x] #3 Any remaining exceptions have explicit rationale in code comments or task notes.
<!-- DOD:END --> <!-- DOD:END -->

View File

@@ -2,74 +2,82 @@
Read first. Keep concise. Read first. Keep concise.
| agent_id | alias | mission | status | file | last_update_utc | | agent_id | alias | mission | status | file | last_update_utc |
| ------------------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------------- | ---------------------- | | ------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------- | -------------------------------------------------------------------------------------- | ---------------------- |
| `codex-aniskip-intro-skip-20260222T080257Z-51fx` | `codex-aniskip-intro-skip` | `Port intro skip to AniSkip API in mpv plugin with OSD skip button` | `handoff` | `docs/subagents/agents/codex-aniskip-intro-skip-20260222T080257Z-51fx.md` | `2026-02-22T10:04:20Z` | | `codex-field-grouping-autoupdate-race-20260222T193915Z-m8p4` | `codex-field-grouping-autoupdate-race` | `Fix Kiku field-grouping merge race with auto-update so note enrichment completes before duplicate merge` | `handoff` | `docs/subagents/agents/codex-field-grouping-autoupdate-race-20260222T193915Z-m8p4.md` | `2026-02-22T19:41:20Z` |
| `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` | | `codex-aniskip-intro-skip-20260222T080257Z-51fx` | `codex-aniskip-intro-skip` | `Port intro skip to AniSkip API in mpv plugin with OSD skip button` | `handoff` | `docs/subagents/agents/codex-aniskip-intro-skip-20260222T080257Z-51fx.md` | `2026-02-22T10:04:20Z` |
| `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` | | `codex-generate-minecard-image-20260220T112900Z-vsxr` | `codex-generate-minecard-image` | `Generate media fallbacks (GIF) from assets/minecard.webm and wire README/docs fallback markup` | `done` | `docs/subagents/agents/codex-generate-minecard-image-20260220T112900Z-vsxr.md` | `2026-02-20T11:35:30Z` |
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` | | `codex-main` | `planner-exec` | `Fix frequency/N+1 regression in plugin --start flow` | `in_progress` | `docs/subagents/agents/codex-main.md` | `2026-02-19T19:36:46Z` |
| `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` | | `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T11:42:39Z` |
| `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` | | `codex-config-validation-20260219T172015Z-iiyf` | `codex-config-validation` | `Find root cause of config validation error for ~/.config/SubMiner/config.jsonc` | `completed` | `docs/subagents/agents/codex-config-validation-20260219T172015Z-iiyf.md` | `2026-02-19T17:26:17Z` |
| `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` | | `codex-task85-20260219T233711Z-46hc` | `codex-task85` | `Resume TASK-85 maintainability refactor from latest handoff point` | `in_progress` | `docs/subagents/agents/codex-task85-20260219T233711Z-46hc.md` | `2026-02-20T02:56:34Z` |
| `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` | | `codex-anilist-deeplink-20260219T233926Z` | `anilist-deeplink` | `Fix external subminer:// AniList callback handling from browser` | `done` | `docs/subagents/agents/codex-anilist-deeplink-20260219T233926Z.md` | `2026-02-19T23:59:21Z` |
| `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` | | `codex-texthooker-highlights-20260220T002354Z-927c` | `codex-texthooker-highlights` | `Add optional texthooker highlight toggles for known/n+1/frequency/JLPT` | `completed` | `docs/subagents/agents/codex-texthooker-highlights-20260220T002354Z-927c.md` | `2026-02-20T00:30:49Z` |
| `codex-texthooker-color-ws-20260220T005844Z-r7m2` | `codex-texthooker-color-ws` | `Fix texthooker websocket payload so token highlight colors render` | `completed` | `docs/subagents/agents/codex-texthooker-color-ws-20260220T005844Z-r7m2.md` | `2026-02-20T01:01:00Z` | | `codex-texthooker-ui-playwright-20260220T003827Z-k3p9` | `codex-texthooker-ui-playwright` | `Run Playwright MCP smoke/regression checks for texthooker-ui changes` | `completed` | `docs/subagents/agents/codex-texthooker-ui-playwright-20260220T003827Z-k3p9.md` | `2026-02-20T00:42:09Z` |
| `codex-nplusone-pos1-20260220T012300Z-c5he` | `codex-nplusone-pos1` | `Fix N+1 false-negative when Yomitan functional tokens inflate unknown candidate count` | `completed` | `docs/subagents/agents/codex-nplusone-pos1-20260220T012300Z-c5he.md` | `2026-02-20T01:28:20Z` | | `codex-texthooker-color-ws-20260220T005844Z-r7m2` | `codex-texthooker-color-ws` | `Fix texthooker websocket payload so token highlight colors render` | `completed` | `docs/subagents/agents/codex-texthooker-color-ws-20260220T005844Z-r7m2.md` | `2026-02-20T01:01:00Z` |
| `codex-subtitle-bg-20260220T054247Z-h9cu` | `codex-subtitle-bg` | `Update default subtitle background color to requested RGBA value` | `completed` | `docs/subagents/agents/codex-subtitle-bg-20260220T054247Z-h9cu.md` | `2026-02-20T05:44:45Z` | | `codex-nplusone-pos1-20260220T012300Z-c5he` | `codex-nplusone-pos1` | `Fix N+1 false-negative when Yomitan functional tokens inflate unknown candidate count` | `completed` | `docs/subagents/agents/codex-nplusone-pos1-20260220T012300Z-c5he.md` | `2026-02-20T01:28:20Z` |
| `codex-narrow-space-tokenizer-20260220T061716Z-p97s` | `codex-narrow-space-tokenizer` | `Fix tokenization when subtitle line contains narrow/invisible Unicode spacing between segments` | `completed` | `docs/subagents/agents/codex-narrow-space-tokenizer-20260220T061716Z-p97s.md` | `2026-02-20T06:20:07Z` | | `codex-subtitle-bg-20260220T054247Z-h9cu` | `codex-subtitle-bg` | `Update default subtitle background color to requested RGBA value` | `completed` | `docs/subagents/agents/codex-subtitle-bg-20260220T054247Z-h9cu.md` | `2026-02-20T05:44:45Z` |
| `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` | | `codex-narrow-space-tokenizer-20260220T061716Z-p97s` | `codex-narrow-space-tokenizer` | `Fix tokenization when subtitle line contains narrow/invisible Unicode spacing between segments` | `completed` | `docs/subagents/agents/codex-narrow-space-tokenizer-20260220T061716Z-p97s.md` | `2026-02-20T06:20:07Z` |
| `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` | | `codex-preserve-linebreaks-20260220T063538Z-s4nd` | `codex-preserve-linebreaks` | `Add config option to preserve subtitle line breaks in visible overlay rendering` | `completed` | `docs/subagents/agents/codex-preserve-linebreaks-20260220T063538Z-s4nd.md` | `2026-02-20T06:42:51Z` |
| `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` | | `codex-release-mpv-plugin-20260220T035757Z-d4yf` | `codex-release-mpv-plugin` | `Package optional release assets bundle (mpv plugin + rofi theme), move theme to assets/themes, update install/docs` | `completed` | `docs/subagents/agents/codex-release-mpv-plugin-20260220T035757Z-d4yf.md` | `2026-02-20T04:02:26Z` |
| `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` | | `codex-bundle-config-example-20260220T092408Z-a1b2` | `codex-bundle-config-example` | `Bundle config.example.jsonc in release assets tarball and align install docs` | `completed` | `docs/subagents/agents/codex-bundle-config-example-20260220T092408Z-a1b2.md` | `2026-02-20T09:26:24Z` |
| `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` | | `codex-tsconfig-modernize-20260220T093035Z-68qb` | `codex-tsconfig-modernize` | `Enable noUncheckedIndexedAccess + isolatedModules in root tsconfig and fix resulting compile errors` | `completed` | `docs/subagents/agents/codex-tsconfig-modernize-20260220T093035Z-68qb.md` | `2026-02-20T09:46:26Z` |
| `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` | | `codex-jellyfin-secret-store-20260220T101428Z-om4z` | `codex-jellyfin-secret-store` | `Verify whether Jellyfin token can use same secret-store path as AniList token` | `completed` | `docs/subagents/agents/codex-jellyfin-secret-store-20260220T101428Z-om4z.md` | `2026-02-20T10:22:45Z` |
| `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` | | `codex-vitepress-subagents-ignore-20260220T101755Z-k2m9` | `codex-vitepress-subagents-ignore` | `Exclude docs/subagents from VitePress build` | `completed` | `docs/subagents/agents/codex-vitepress-subagents-ignore-20260220T101755Z-k2m9.md` | `2026-02-20T10:18:30Z` |
| `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-21T02:04:12Z` | | `codex-preserve-linebreak-display-20260220T110436Z-r8f1` | `codex-preserve-linebreak-display` | `Fix visible overlay display artifact when subtitleStyle.preserveLineBreaks is disabled` | `completed` | `docs/subagents/agents/codex-preserve-linebreak-display-20260220T110436Z-r8f1.md` | `2026-02-20T11:10:51Z` |
| `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` | | `codex-review-refactor-cleanup-20260220T113818Z-i2ov` | `codex-review-refactor-cleanup` | `Review recent TASK-85 refactor effort and identify remaining cleanup work` | `handoff` | `docs/subagents/agents/codex-review-refactor-cleanup-20260220T113818Z-i2ov.md` | `2026-02-21T02:04:12Z` |
| `codex-task95-hotspots-20260221T031420Z-x7k2` | `codex-task95-hotspots` | `Execute TASK-95 decompose oversized core hotspots end-to-end` | `done` | `docs/subagents/agents/codex-task95-hotspots-20260221T031420Z-x7k2.md` | `2026-02-21T03:29:23Z` | | `codex-commit-unstaged-20260220T115057Z-k7q2` | `codex-commit-unstaged` | `Commit all current unstaged repository changes with content-derived conventional message` | `in_progress` | `docs/subagents/agents/codex-commit-unstaged-20260220T115057Z-k7q2.md` | `2026-02-20T11:51:18Z` |
| `codex-task94-thin-root-20260221T031320Z-q3n7` | `codex-task94-thin-root` | `Execute TASK-94 by extracting main.ts deps-builder clusters into runtime composers` | `handoff` | `docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md` | `2026-02-21T03:41:10Z` | | `codex-task95-hotspots-20260221T031420Z-x7k2` | `codex-task95-hotspots` | `Execute TASK-95 decompose oversized core hotspots end-to-end` | `done` | `docs/subagents/agents/codex-task95-hotspots-20260221T031420Z-x7k2.md` | `2026-02-21T03:29:23Z` |
| `opencode-task95-immersion-tracker-20260221T031846Z-p4k9` | `opencode-task95-immersion-tracker` | `Implement TASK-95 immersion-tracker extraction into focused collaborators and seam tests` | `handoff` | `docs/subagents/agents/opencode-task95-immersion-tracker-20260221T031846Z-p4k9.md` | `2026-02-21T03:26:51Z` | | `codex-task94-thin-root-20260221T031320Z-q3n7` | `codex-task94-thin-root` | `Execute TASK-94 by extracting main.ts deps-builder clusters into runtime composers` | `handoff` | `docs/subagents/agents/codex-task94-thin-root-20260221T031320Z-q3n7.md` | `2026-02-21T03:41:10Z` |
| `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` | | `opencode-task95-immersion-tracker-20260221T031846Z-p4k9` | `opencode-task95-immersion-tracker` | `Implement TASK-95 immersion-tracker extraction into focused collaborators and seam tests` | `handoff` | `docs/subagents/agents/opencode-task95-immersion-tracker-20260221T031846Z-p4k9.md` | `2026-02-21T03:26:51Z` |
| `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` | | `opencode-task95-config-20260221T031843Z-m4k9` | `opencode-task95-config` | `Implement TASK-95 config extraction for src/config/service.ts` | `done` | `docs/subagents/agents/opencode-task95-config-20260221T031843Z-m4k9.md` | `2026-02-21T03:26:57Z` |
| `opencode-task-94-20260221T033647Z-7ou2` | `opencode-task-94` | `Finish TASK-94 thin composition root refactor and close acceptance criteria` | `done` | `docs/subagents/agents/opencode-task-94-20260221T033647Z-7ou2.md` | `2026-02-21T04:12:45Z` | | `codex-task95-anki-20260221T031836Z-6f3e` | `codex-task95-anki` | `Implement TASK-95 anki-integration extraction for field-grouping merge collaborator` | `done` | `docs/subagents/agents/codex-task95-anki-20260221T031836Z-6f3e.md` | `2026-02-21T03:26:55Z` |
| `codex-task71-round2-20260221T043541Z-k9t3` | `codex-task71-round2` | `Execute TASK-71 round 2 split of main.ts into domain runtime modules` | `done` | `docs/subagents/agents/codex-task71-round2-20260221T043541Z-k9t3.md` | `2026-02-21T04:57:00Z` | | `opencode-task-94-20260221T033647Z-7ou2` | `opencode-task-94` | `Finish TASK-94 thin composition root refactor and close acceptance criteria` | `done` | `docs/subagents/agents/opencode-task-94-20260221T033647Z-7ou2.md` | `2026-02-21T04:12:45Z` |
| `codex-task85-20260221T051308Z-164g` | `codex-task85-exec` | `Execute TASK-85 remaining AC/DoD with writing-plans and executing-plans flow` | `done` | `docs/subagents/agents/codex-task85-20260221T051308Z-164g.md` | `2026-02-21T05:45:50Z` | | `codex-task71-round2-20260221T043541Z-k9t3` | `codex-task71-round2` | `Execute TASK-71 round 2 split of main.ts into domain runtime modules` | `done` | `docs/subagents/agents/codex-task71-round2-20260221T043541Z-k9t3.md` | `2026-02-21T04:57:00Z` |
| `codex-review-refactor-20260221T062353Z-p6k2` | `codex-review-refactor` | `Perform code review for current refactor changes and report actionable findings` | `done` | `docs/subagents/agents/codex-review-refactor-20260221T062353Z-p6k2.md` | `2026-02-21T07:16:33Z` | | `codex-task85-20260221T051308Z-164g` | `codex-task85-exec` | `Execute TASK-85 remaining AC/DoD with writing-plans and executing-plans flow` | `done` | `docs/subagents/agents/codex-task85-20260221T051308Z-164g.md` | `2026-02-21T05:45:50Z` |
| `opencode-task93-sync-20260221T070842Z-71c6` | `opencode-task93-sync` | `Synchronize TASK-85 closure tracking and child-task status in Backlog` | `done` | `docs/subagents/agents/opencode-task93-sync-20260221T070842Z-71c6.md` | `2026-02-21T07:11:58Z` | | `codex-review-refactor-20260221T062353Z-p6k2` | `codex-review-refactor` | `Perform code review for current refactor changes and report actionable findings` | `done` | `docs/subagents/agents/codex-review-refactor-20260221T062353Z-p6k2.md` | `2026-02-21T07:16:33Z` |
| `opencode-task97-runtime-composer-20260221T094150Z-r8k3` | `opencode-task97-runtime-composer` | `Execute TASK-97 normalize runtime composer contracts end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task97-runtime-composer-20260221T094150Z-r8k3.md` | `2026-02-21T10:06:59Z` | | `opencode-task93-sync-20260221T070842Z-71c6` | `opencode-task93-sync` | `Synchronize TASK-85 closure tracking and child-task status in Backlog` | `done` | `docs/subagents/agents/opencode-task93-sync-20260221T070842Z-71c6.md` | `2026-02-21T07:11:58Z` |
| `opencode-task96-config-resolve-20260221T094119Z-mbfo` | `opencode-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules with plan-first workflow` | `planning` | `docs/subagents/agents/opencode-task96-config-resolve-20260221T094119Z-mbfo.md` | `2026-02-21T09:41:19Z` | | `opencode-task97-runtime-composer-20260221T094150Z-r8k3` | `opencode-task97-runtime-composer` | `Execute TASK-97 normalize runtime composer contracts end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task97-runtime-composer-20260221T094150Z-r8k3.md` | `2026-02-21T10:06:59Z` |
| `opencode-task98-source-tests-20260221T094524Z-kzvd` | `opencode-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling without commit` | `blocked` | `docs/subagents/agents/opencode-task98-source-tests-20260221T094524Z-kzvd.md` | `2026-02-21T09:56:47Z` | | `opencode-task96-config-resolve-20260221T094119Z-mbfo` | `opencode-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules with plan-first workflow` | `planning` | `docs/subagents/agents/opencode-task96-config-resolve-20260221T094119Z-mbfo.md` | `2026-02-21T09:41:19Z` |
| `codex-task96-config-resolve-20260221T110058Z-k7m2` | `codex-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules end-to-end without commit` | `done` | `docs/subagents/agents/codex-task96-config-resolve-20260221T110058Z-k7m2.md` | `2026-02-21T20:10:43Z` | | `opencode-task98-source-tests-20260221T094524Z-kzvd` | `opencode-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling without commit` | `blocked` | `docs/subagents/agents/opencode-task98-source-tests-20260221T094524Z-kzvd.md` | `2026-02-21T09:56:47Z` |
| `codex-task73-mpv-socket-20260221T201605Z-zjhs` | `codex-task73-mpv-socket` | `Execute TASK-73 consolidate launcher mpv socket readiness primitives end-to-end` | `done` | `docs/subagents/agents/codex-task73-mpv-socket-20260221T201605Z-zjhs.md` | `2026-02-21T20:20:18Z` | | `codex-task96-config-resolve-20260221T110058Z-k7m2` | `codex-task96-config-resolve` | `Execute TASK-96 split config resolve into domain modules end-to-end without commit` | `done` | `docs/subagents/agents/codex-task96-config-resolve-20260221T110058Z-k7m2.md` | `2026-02-21T20:10:43Z` |
| `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` | | `codex-task73-mpv-socket-20260221T201605Z-zjhs` | `codex-task73-mpv-socket` | `Execute TASK-73 consolidate launcher mpv socket readiness primitives end-to-end` | `done` | `docs/subagents/agents/codex-task73-mpv-socket-20260221T201605Z-zjhs.md` | `2026-02-21T20:20:18Z` |
| `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` | | `codex-task74-launcher-tests-20260221T201635Z-10i6` | `codex-task74-launcher-tests` | `Implement TASK-74 launcher regression tests for config discovery + command branching end-to-end` | `done` | `docs/subagents/agents/codex-task74-launcher-tests-20260221T201635Z-10i6.md` | `2026-02-21T20:20:52Z` |
| `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` | | `opencode-task76-anki-workflows-20260221T201659Z-r4p1` | `opencode-task76-anki-workflows` | `Execute TASK-76 decompose anki-integration orchestrator into workflow services via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task76-anki-workflows-20260221T201659Z-r4p1.md` | `2026-02-21T21:17:28Z` |
| `codex-docs-unpushed-review-20260221T213707Z-lyej` | `codex-docs-unpushed-review` | `Review unpushed commits for docs drift; patch docs to reflect current code/state` | `done` | `docs/subagents/agents/codex-docs-unpushed-review-20260221T213707Z-lyej.md` | `2026-02-21T21:39:15Z` | | `opencode-task76-doc-boundaries-20260221T203558Z-h7q4` | `opencode-task76-doc-boundaries` | `Update Anki integration docs with post-decomposition ownership boundaries for TASK-76` | `done` | `docs/subagents/agents/opencode-task76-doc-boundaries-20260221T203558Z-h7q4.md` | `2026-02-21T20:36:55Z` |
| `codex-task72-strict-startup-config-20260221T231804Z-3ngd` | `codex-task72-strict-startup-config` | `Execute TASK-72 strict startup config loading with actionable user-facing errors` | `done` | `docs/subagents/agents/codex-task72-strict-startup-config-20260221T231804Z-3ngd.md` | `2026-02-21T23:26:29Z` | | `codex-docs-unpushed-review-20260221T213707Z-lyej` | `codex-docs-unpushed-review` | `Review unpushed commits for docs drift; patch docs to reflect current code/state` | `done` | `docs/subagents/agents/codex-docs-unpushed-review-20260221T213707Z-lyej.md` | `2026-02-21T21:39:15Z` |
| `opencode-task77-tokenizer-stages-20260221T232016Z-v9k2` | `opencode-task77-tokenizer-stages` | `Execute TASK-77 tokenizer pipeline split into parser-selection enrichment and annotation stages without commit` | `done` | `docs/subagents/agents/opencode-task77-tokenizer-stages-20260221T232016Z-v9k2.md` | `2026-02-21T23:47:08Z` | | `codex-task72-strict-startup-config-20260221T231804Z-3ngd` | `codex-task72-strict-startup-config` | `Execute TASK-72 strict startup config loading with actionable user-facing errors` | `done` | `docs/subagents/agents/codex-task72-strict-startup-config-20260221T231804Z-3ngd.md` | `2026-02-21T23:26:29Z` |
| `codex-task75-mpv-osd-buffered-20260221T231816Z-yj32` | `codex-task75-mpv-osd-buffered` | `Execute TASK-75 move MPV OSD log writes to buffered async path end-to-end` | `done` | `docs/subagents/agents/codex-task75-mpv-osd-buffered-20260221T231816Z-yj32.md` | `2026-02-21T23:48:10Z` | | `opencode-task77-tokenizer-stages-20260221T232016Z-v9k2` | `opencode-task77-tokenizer-stages` | `Execute TASK-77 tokenizer pipeline split into parser-selection enrichment and annotation stages without commit` | `done` | `docs/subagents/agents/opencode-task77-tokenizer-stages-20260221T232016Z-v9k2.md` | `2026-02-21T23:47:08Z` |
| `opencode-task72-strict-startup-config-20260221T232155Z-kf0o` | `opencode-task72-strict-startup-config` | `Implement Task 1 from strict startup config loading plan with startup malformed-config failure signal and tests` | `done` | `docs/subagents/agents/opencode-task72-strict-startup-config-20260221T232155Z-kf0o.md` | `2026-02-21T23:24:32Z` | | `codex-task75-mpv-osd-buffered-20260221T231816Z-yj32` | `codex-task75-mpv-osd-buffered` | `Execute TASK-75 move MPV OSD log writes to buffered async path end-to-end` | `done` | `docs/subagents/agents/codex-task75-mpv-osd-buffered-20260221T231816Z-yj32.md` | `2026-02-21T23:48:10Z` |
| `opencode-task72-parse-details-20260221T232137Z-b63t` | `opencode-task72-parse-details` | `Implement TASK-72 Task 2 shared parse-error formatter wiring and tests` | `done` | `docs/subagents/agents/opencode-task72-parse-details-20260221T232137Z-b63t.md` | `2026-02-21T23:24:12Z` | | `opencode-task72-strict-startup-config-20260221T232155Z-kf0o` | `opencode-task72-strict-startup-config` | `Implement Task 1 from strict startup config loading plan with startup malformed-config failure signal and tests` | `done` | `docs/subagents/agents/opencode-task72-strict-startup-config-20260221T232155Z-kf0o.md` | `2026-02-21T23:24:32Z` |
| `opencode-task77-slice-a-20260222T000100Z-j4p2` | `opencode-task77-slice-a` | `Implement TASK-77 slice A parser-selection-stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-slice-a-20260222T000100Z-j4p2.md` | `2026-02-22T00:03:30Z` | | `opencode-task72-parse-details-20260221T232137Z-b63t` | `opencode-task72-parse-details` | `Implement TASK-72 Task 2 shared parse-error formatter wiring and tests` | `done` | `docs/subagents/agents/opencode-task72-parse-details-20260221T232137Z-b63t.md` | `2026-02-21T23:24:12Z` |
| `opencode-task78-config-domain-20260221T235604Z-p9x2` | `opencode-task78-config-domain` | `Execute TASK-78 modularize config definitions and validation by domain end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task78-config-domain-20260221T235604Z-p9x2.md` | `2026-02-22T00:06:30Z` | | `opencode-task77-slice-a-20260222T000100Z-j4p2` | `opencode-task77-slice-a` | `Implement TASK-77 slice A parser-selection-stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-slice-a-20260222T000100Z-j4p2.md` | `2026-02-22T00:03:30Z` |
| `opencode-task77-sliceb-20260221T232507Z-vzk5` | `opencode-task77-sliceb` | `Implement TASK-77 slice B parser-enrichment stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-sliceb-20260221T232507Z-vzk5.md` | `2026-02-21T23:27:40Z` | | `opencode-task78-config-domain-20260221T235604Z-p9x2` | `opencode-task78-config-domain` | `Execute TASK-78 modularize config definitions and validation by domain end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task78-config-domain-20260221T235604Z-p9x2.md` | `2026-02-22T00:06:30Z` |
| `opencode-task79-runtime-reducers-20260221T235652Z-n4p7` | `opencode-task79-runtime-reducers` | `Execute TASK-79 explicit runtime state transitions/reducers in main via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task79-runtime-reducers-20260221T235652Z-n4p7.md` | `2026-02-22T00:10:51Z` | | `opencode-task77-sliceb-20260221T232507Z-vzk5` | `opencode-task77-sliceb` | `Implement TASK-77 slice B parser-enrichment stage module + focused tests without touching tokenizer.ts` | `done` | `docs/subagents/agents/opencode-task77-sliceb-20260221T232507Z-vzk5.md` | `2026-02-21T23:27:40Z` |
| `opencode-task79-sliceb-20260222T000253Z-m2r7` | `opencode-task79-sliceb` | `Implement TASK-79 slice B invariants coverage/tests and composition-boundary docs updates without commit` | `done` | `docs/subagents/agents/opencode-task79-sliceb-20260222T000253Z-m2r7.md` | `2026-02-22T00:04:21Z` | | `opencode-task79-runtime-reducers-20260221T235652Z-n4p7` | `opencode-task79-runtime-reducers` | `Execute TASK-79 explicit runtime state transitions/reducers in main via plan-first workflow` | `done` | `docs/subagents/agents/opencode-task79-runtime-reducers-20260221T235652Z-n4p7.md` | `2026-02-22T00:10:51Z` |
| `opencode-task80-ipc-contract-20260222T001728Z-obrv` | `opencode-task80-ipc-contract` | `Execute TASK-80 IPC contract typing + runtime payload validation end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task80-ipc-contract-20260222T001728Z-obrv.md` | `2026-02-22T00:56:00Z` | | `opencode-task79-sliceb-20260222T000253Z-m2r7` | `opencode-task79-sliceb` | `Implement TASK-79 slice B invariants coverage/tests and composition-boundary docs updates without commit` | `done` | `docs/subagents/agents/opencode-task79-sliceb-20260222T000253Z-m2r7.md` | `2026-02-22T00:04:21Z` |
| `opencode-task82-smoke-20260222T002150Z-p5bp` | `opencode-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task82-smoke-20260222T002150Z-p5bp.md` | `2026-02-22T00:54:29Z` | | `opencode-task80-ipc-contract-20260222T001728Z-obrv` | `opencode-task80-ipc-contract` | `Execute TASK-80 IPC contract typing + runtime payload validation end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task80-ipc-contract-20260222T001728Z-obrv.md` | `2026-02-22T00:56:00Z` |
| `codex-task82-smoke-20260222T002523Z-3j7u` | `codex-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/codex-task82-smoke-20260222T002523Z-3j7u.md` | `2026-02-22T00:53:25Z` | | `opencode-task82-smoke-20260222T002150Z-p5bp` | `opencode-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task82-smoke-20260222T002150Z-p5bp.md` | `2026-02-22T00:54:29Z` |
| `opencode-task81-launcher-modules-20260222T005725Z-8oh8` | `opencode-task81-launcher-modules` | `Execute TASK-81 launcher command-module/process-adapter refactor end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task81-launcher-modules-20260222T005725Z-8oh8.md` | `2026-02-22T01:09:30Z` | | `codex-task82-smoke-20260222T002523Z-3j7u` | `codex-task82-smoke` | `Execute TASK-82 e2e smoke suite for launcher/mpv/ipc/overlay end-to-end without commit` | `done` | `docs/subagents/agents/codex-task82-smoke-20260222T002523Z-3j7u.md` | `2026-02-22T00:53:25Z` |
| `codex-task99-guardrails-20260222T010930Z-m9q2` | `codex-task99-guardrails` | `Execute TASK-99 maintainability guardrail expansion and runtime cycle checks end-to-end without commit` | `done` | `docs/subagents/agents/codex-task99-guardrails-20260222T010930Z-m9q2.md` | `2026-02-22T03:01:34Z` | | `opencode-task81-launcher-modules-20260222T005725Z-8oh8` | `opencode-task81-launcher-modules` | `Execute TASK-81 launcher command-module/process-adapter refactor end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task81-launcher-modules-20260222T005725Z-8oh8.md` | `2026-02-22T01:09:30Z` |
| `opencode-task84-keybindings-gating-20260222T011624Z-llor` | `opencode-task84-keybindings-gating` | `Execute TASK-84 gate feature-dependent keybindings behind config flags end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task84-keybindings-gating-20260222T011624Z-llor.md` | `2026-02-22T01:35:30Z` | | `codex-task99-guardrails-20260222T010930Z-m9q2` | `codex-task99-guardrails` | `Execute TASK-99 maintainability guardrail expansion and runtime cycle checks end-to-end without commit` | `done` | `docs/subagents/agents/codex-task99-guardrails-20260222T010930Z-m9q2.md` | `2026-02-22T03:01:34Z` |
| `codex-task98-source-tests-20260222T021156Z-a1b2` | `codex-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling end-to-end without commit` | `done` | `docs/subagents/agents/codex-task98-source-tests-20260222T021156Z-a1b2.md` | `2026-02-22T02:36:00Z` | | `opencode-task84-keybindings-gating-20260222T011624Z-llor` | `opencode-task84-keybindings-gating` | `Execute TASK-84 gate feature-dependent keybindings behind config flags end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task84-keybindings-gating-20260222T011624Z-llor.md` | `2026-02-22T01:35:30Z` |
| `codex-task101-docs-archive-20260222T024156Z-hneu` | `codex-task101-docs-archive` | `Execute TASK-101 consolidate architecture docs and archive task-noise evidence` | `done` | `docs/subagents/agents/codex-task101-docs-archive-20260222T024156Z-hneu.md` | `2026-02-22T03:01:38Z` | | `codex-task98-source-tests-20260222T021156Z-a1b2` | `codex-task98-source-tests` | `Execute TASK-98 shift core tests to source level and trim dist coupling end-to-end without commit` | `done` | `docs/subagents/agents/codex-task98-source-tests-20260222T021156Z-a1b2.md` | `2026-02-22T02:36:00Z` |
| `codex-fix-ci-20260222T025848Z-0xdl` | `codex-fix-ci` | `Inspect failing GitHub Actions PR checks and implement approved fix` | `done` | `docs/subagents/agents/codex-fix-ci-20260222T025848Z-0xdl.md` | `2026-02-22T03:25:40Z` | | `codex-task101-docs-archive-20260222T024156Z-hneu` | `codex-task101-docs-archive` | `Execute TASK-101 consolidate architecture docs and archive task-noise evidence` | `done` | `docs/subagents/agents/codex-task101-docs-archive-20260222T024156Z-hneu.md` | `2026-02-22T03:01:38Z` |
| `opencode-task100-dead-code-prune-20260222T033155Z-qenz` | `opencode-task100-dead-code-prune` | `Execute TASK-100 dead-code prune and cleanup via writing-plans + executing-plans (no commit)` | `done` | `docs/subagents/agents/opencode-task100-dead-code-prune-20260222T033155Z-qenz.md` | `2026-02-22T04:00:41Z` | | `codex-fix-ci-20260222T025848Z-0xdl` | `codex-fix-ci` | `Inspect failing GitHub Actions PR checks and implement approved fix` | `done` | `docs/subagents/agents/codex-fix-ci-20260222T025848Z-0xdl.md` | `2026-02-22T03:25:40Z` |
| `codex-gh-fix-ci-20260222T054631Z-m4t8` | `codex-gh-fix-ci` | `Inspect failing GitHub Actions checks; propose fix plan before implementation` | `done` | `docs/subagents/agents/codex-gh-fix-ci-20260222T054631Z-m4t8.md` | `2026-02-22T06:11:15Z` | | `opencode-task100-dead-code-prune-20260222T033155Z-qenz` | `opencode-task100-dead-code-prune` | `Execute TASK-100 dead-code prune and cleanup via writing-plans + executing-plans (no commit)` | `done` | `docs/subagents/agents/opencode-task100-dead-code-prune-20260222T033155Z-qenz.md` | `2026-02-22T04:00:41Z` |
| `codex-fix-rebase-errors-20260222T062235Z-73h4` | `codex-fix-rebase-errors` | `Resolve current git rebase conflicts in ipc/main runtime files and land clean rebase state` | `done` | `docs/subagents/agents/codex-fix-rebase-errors-20260222T062235Z-73h4.md` | `2026-02-22T06:30:48Z` | | `codex-gh-fix-ci-20260222T054631Z-m4t8` | `codex-gh-fix-ci` | `Inspect failing GitHub Actions checks; propose fix plan before implementation` | `done` | `docs/subagents/agents/codex-gh-fix-ci-20260222T054631Z-m4t8.md` | `2026-02-22T06:11:15Z` |
| `codex-review-cleanup-20260222T065718Z-9p4m` | `codex-review-cleanup` | `Review post-refactor codebase quality and create cleanup tickets with concrete scope and completion criteria` | `done` | `docs/subagents/agents/codex-review-cleanup-20260222T065718Z-9p4m.md` | `2026-02-22T07:04:48Z` | | `codex-fix-rebase-errors-20260222T062235Z-73h4` | `codex-fix-rebase-errors` | `Resolve current git rebase conflicts in ipc/main runtime files and land clean rebase state` | `done` | `docs/subagents/agents/codex-fix-rebase-errors-20260222T062235Z-73h4.md` | `2026-02-22T06:30:48Z` |
| `codex-jellyfin-ts-fix-20260222T071530Z-5e50` | `codex-jellyfin-ts-fix` | `Fix Jellyfin token/session type drift causing TS compile failures in config+main.` | `done` | `docs/subagents/agents/codex-jellyfin-ts-fix-20260222T071530Z-5e50.md` | `2026-02-22T07:23:47Z` | | `codex-review-cleanup-20260222T065718Z-9p4m` | `codex-review-cleanup` | `Review post-refactor codebase quality and create cleanup tickets with concrete scope and completion criteria` | `done` | `docs/subagents/agents/codex-review-cleanup-20260222T065718Z-9p4m.md` | `2026-02-22T07:04:48Z` |
| `codex-overlay-toggle-regression-20260222T073450Z-q7m4` | `codex-overlay-toggle-regression` | `Fix post-rebase overlay toggle regression causing transparent non-interactable windows and broken keybinds (TASK-107).` | `testing` | `docs/subagents/agents/codex-overlay-toggle-regression-20260222T073450Z-q7m4.md` | `2026-02-22T07:45:58Z` | | `codex-jellyfin-ts-fix-20260222T071530Z-5e50` | `codex-jellyfin-ts-fix` | `Fix Jellyfin token/session type drift causing TS compile failures in config+main.` | `done` | `docs/subagents/agents/codex-jellyfin-ts-fix-20260222T071530Z-5e50.md` | `2026-02-22T07:23:47Z` |
| `codex-docs-review-20260222T094009Z-g8p2` | `codex-docs-review` | `Review README/docs for drift vs current code/scripts; patch stale or missing documentation.` | `done` | `docs/subagents/agents/codex-docs-review-20260222T094009Z-g8p2.md` | `2026-02-22T09:43:52Z` | | `codex-overlay-toggle-regression-20260222T073450Z-q7m4` | `codex-overlay-toggle-regression` | `Fix post-rebase overlay toggle regression causing transparent non-interactable windows and broken keybinds (TASK-107).` | `testing` | `docs/subagents/agents/codex-overlay-toggle-regression-20260222T073450Z-q7m4.md` | `2026-02-22T07:45:58Z` |
| `codex-docs-review-20260222T094009Z-g8p2` | `codex-docs-review` | `Review README/docs for drift vs current code/scripts; patch stale or missing documentation.` | `done` | `docs/subagents/agents/codex-docs-review-20260222T094009Z-g8p2.md` | `2026-02-22T09:43:52Z` |
| `codex-discord-presence-task-20260222T194048Z-d7k2` | `codex-discord-presence-task` | `Add backlog task for Discord Rich Presence integration with polished activity card` | `done` | `docs/subagents/agents/codex-discord-presence-task-20260222T194048Z-d7k2.md` | `2026-02-22T19:41:00Z` |
| `codex-task108-aniskip-20260222T194600Z-qgdt` | `codex-task108-aniskip` | `Execute TASK-108 end-to-end with plan-first workflow and no commit` | `done` | `docs/subagents/agents/codex-task108-aniskip-20260222T194600Z-qgdt.md` | `2026-02-22T19:49:30Z` |
| `codex-gh-fix-ci-20260222T191948Z-b7n4` | `codex-gh-fix-ci` | `Triage failing GitHub Actions checks on active PR; summarize root cause; propose fix plan before edits` | `handoff` | `docs/subagents/agents/codex-gh-fix-ci-20260222T191948Z-b7n4.md` | `2026-02-22T19:24:12Z` | | `codex-gh-fix-ci-20260222T191948Z-b7n4` | `codex-gh-fix-ci` | `Triage failing GitHub Actions checks on active PR; summarize root cause; propose fix plan before edits` | `handoff` | `docs/subagents/agents/codex-gh-fix-ci-20260222T191948Z-b7n4.md` | `2026-02-22T19:24:12Z` |
| `opencode-task105-unsafe-casts-20260222T194704Z-zfcm` | `opencode-task105-unsafe-casts` | `Execute TASK-105 eliminate unsafe non-test runtime casts in main boundaries end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md` | `2026-02-22T21:56:30Z` |
| `codex-task104-launcher-config-20260222T194708Z-z9x1` | `codex-task104-launcher-config` | `Execute TASK-104 end-to-end with plan-first workflow and no commit` | `done` | `docs/subagents/agents/codex-task104-launcher-config-20260222T194708Z-z9x1.md` | `2026-02-22T19:56:26Z` |
| `opencode-task106-immersion-modules-20260222T195109Z-r3m7` | `opencode-task106-immersion-modules` | `Execute TASK-106 decomposition of immersion tracker service into storage session and metadata modules end-to-end without commit` | `done` | `docs/subagents/agents/opencode-task106-immersion-modules-20260222T195109Z-r3m7.md` | `2026-02-22T21:58:45Z` |
| `codex-task105-sliceb-20260222T195423Z-w8n3` | `codex-task105-sliceb` | `Implement TASK-105 slice B runtime cast removal in targeted main/runtime modules without commit` | `done` | `docs/subagents/agents/codex-task105-sliceb-20260222T195423Z-w8n3.md` | `2026-02-22T20:02:06Z` |
| `codex-ts-build-errors-20260222T215411Z-h3k7` | `codex-ts-build-errors` | `Fix current TypeScript build failures in anki/runtime tests and deps typing contracts; keep behavior unchanged.` | `done` | `docs/subagents/agents/codex-ts-build-errors-20260222T215411Z-h3k7.md` | `2026-02-22T21:55:54Z` |

View File

@@ -0,0 +1,78 @@
# Agent Session: opencode-task105-unsafe-casts-20260222T194704Z-zfcm
- alias: `opencode-task105-unsafe-casts`
- mission: `Execute TASK-105 eliminate unsafe non-test runtime casts in main boundaries end-to-end without commit.`
- status: `done`
- started_utc: `2026-02-22T19:47:35Z`
- last_update_utc: `2026-02-22T21:56:30Z`
## Intent
- Load TASK-105 context from Backlog MCP and collect exact unsafe cast hotspots.
- Write implementation plan via writing-plans skill with test-first steps and verification gates.
- Execute plan with executing-plans skill, parallelizing independent slices where safe.
## Planned Files
- `src/main.ts`
- `src/main/runtime/**/*.ts`
- `src/main/**/*.test.ts`
- `docs/plans/*.md`
## Files Touched
- `docs/subagents/INDEX.md`
- `docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md`
- `docs/subagents/collaboration.md`
- `docs/plans/2026-02-22-task-105-unsafe-runtime-casts.md`
- `src/main.ts`
- `src/main/runtime/app-runtime-main-deps.ts`
- `src/main/runtime/cli-command-context-main-deps.ts`
- `src/main/runtime/dictionary-runtime-main-deps.ts`
- `src/main/runtime/field-grouping-overlay-main-deps.ts`
- `src/main/runtime/jellyfin-client-info.ts`
- `src/main/runtime/jellyfin-playback-launch.ts`
- `src/main/runtime/mpv-client-runtime-service.ts`
- `src/main/runtime/mpv-jellyfin-defaults.ts`
- `src/main/runtime/mpv-main-event-bindings.ts`
- `src/main/runtime/mpv-osd-log-main-deps.ts`
- `src/main/runtime/overlay-runtime-bootstrap-handlers.ts`
- `src/main/runtime/overlay-runtime-options-main-deps.ts`
- `src/main/runtime/overlay-runtime-options.ts`
- `src/main/runtime/overlay-visibility-runtime-main-deps.ts`
- `src/main/runtime/startup-config-main-deps.ts`
- `src/main/runtime/subtitle-tokenization-main-deps.ts`
- `src/main/runtime/tray-runtime-handlers.ts`
- `src/main/runtime/cli-command-context-factory.test.ts`
- `src/main/runtime/composers/app-ready-composer.test.ts`
- `src/main/runtime/composers/mpv-runtime-composer.test.ts`
- `src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts`
- `src/main/runtime/overlay-runtime-options-main-deps.test.ts`
- `src/main/runtime/cli-command-context-main-deps.test.ts`
- `src/main/runtime/subtitle-tokenization-main-deps.test.ts`
- `src/main/runtime/jellyfin-playback-launch-main-deps.test.ts`
- `src/main/runtime/jellyfin-playback-launch.test.ts`
- `src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts`
- `src/main/runtime/mpv-jellyfin-defaults.test.ts`
- `src/main/runtime/field-grouping-overlay-main-deps.test.ts`
- `src/main/runtime/mpv-osd-log-main-deps.test.ts`
- `src/main/runtime/overlay-visibility-runtime-main-deps.test.ts`
- `src/main/runtime/startup-config-main-deps.test.ts`
- `backlog/tasks/task-105 - Eliminate-unsafe-non-test-runtime-casts-in-main-boundaries.md`
## Assumptions
- TASK-105 scope excludes test-only casts and non-main-runtime paths.
- Existing compile/test lanes are authoritative for regression detection.
## Phase Log
- `2026-02-22T19:47:35Z` Session started; loaded backlog workflow overview, subagent coordination docs, and TASK-105 details.
- `2026-02-22T19:52:30Z` Wrote plan at `docs/plans/2026-02-22-task-105-unsafe-runtime-casts.md`, set TASK-105 In Progress, and executed runtime cast-elimination with parallel subagents.
- `2026-02-22T21:10:04Z` Completed cast elimination in non-test target scope; follow-up type harmonization done for tray/bootstrap/test contracts.
- `2026-02-22T21:55:38Z` Verification complete: cast scan after=0, targeted runtime tests green, `test:core:src` green; temporarily blocked on unrelated `note-update-workflow.test.ts` TS errors during `bun run build`.
- `2026-02-22T21:56:30Z` Re-ran `bun run build` after concurrent compile-fix; build passes. TASK-105 finalized Done in Backlog with AC/DoD evidence.
## Next Step
- TASK-105 complete. Optional next step: stage/commit focused TASK-105 diff once user requests commit.

View File

@@ -103,6 +103,7 @@ Shared notes. Append-only.
- [2026-02-22T07:45:58Z] [codex-overlay-toggle-regression-20260222T073450Z-q7m4|codex-overlay-toggle-regression] added explicit overlay BrowserWindow sandbox guard (`webPreferences.sandbox=false`) to avoid preload API break on newer Electron defaults; added regression test `src/core/services/overlay-window-config.test.ts`; verified focused tests + build green. - [2026-02-22T07:45:58Z] [codex-overlay-toggle-regression-20260222T073450Z-q7m4|codex-overlay-toggle-regression] added explicit overlay BrowserWindow sandbox guard (`webPreferences.sandbox=false`) to avoid preload API break on newer Electron defaults; added regression test `src/core/services/overlay-window-config.test.ts`; verified focused tests + build green.
## 2026-02-22 ## 2026-02-22
- [2026-02-22T08:04:10Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] starting feature: port intro skip to AniSkip API in `plugin/subminer.lua` with chapter markers + in-range OSD skip button; scoped to mpv plugin/docs only. - [2026-02-22T08:04:10Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] starting feature: port intro skip to AniSkip API in `plugin/subminer.lua` with chapter markers + in-range OSD skip button; scoped to mpv plugin/docs only.
- [2026-02-22T08:05:38Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] implemented AniSkip OP lookup + chapter markers + in-range OSD skip prompt/key in `plugin/subminer.lua`; updated plugin conf/docs; syntax check `luac -p plugin/subminer.lua` passed. - [2026-02-22T08:05:38Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] implemented AniSkip OP lookup + chapter markers + in-range OSD skip prompt/key in `plugin/subminer.lua`; updated plugin conf/docs; syntax check `luac -p plugin/subminer.lua` passed.
- [2026-02-22T08:13:40Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] follow-up applied: launcher now runs `guessit` for file playback and passes AniSkip title/season/episode script-opts (fallback title from filename), and intro hint now displays for first 3s from intro start (`You can skip by pressing y-k`). - [2026-02-22T08:13:40Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] follow-up applied: launcher now runs `guessit` for file playback and passes AniSkip title/season/episode script-opts (fallback title from filename), and intro hint now displays for first 3s from intro start (`You can skip by pressing y-k`).
@@ -116,3 +117,20 @@ Shared notes. Append-only.
- [2026-02-22T10:04:20Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] launcher metadata fix: prefer guessit `series` for AniSkip title and fallback to show-directory extraction (`.../<Show>/Season-*`) instead of episode filename title. - [2026-02-22T10:04:20Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] launcher metadata fix: prefer guessit `series` for AniSkip title and fallback to show-directory extraction (`.../<Show>/Season-*`) instead of episode filename title.
- [2026-02-22T10:10:30Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] plugin hardening: path-derived show title now prioritized over script-opt title for AniSkip lookups, reducing dependence on launcher metadata correctness. - [2026-02-22T10:10:30Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] plugin hardening: path-derived show title now prioritized over script-opt title for AniSkip lookups, reducing dependence on launcher metadata correctness.
- [2026-02-22T10:14:40Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] simplified MAL resolution policy to first-result selection (no score/reject) per user preference. - [2026-02-22T10:14:40Z] [codex-aniskip-intro-skip-20260222T080257Z-51fx|codex-aniskip-intro-skip] simplified MAL resolution policy to first-result selection (no score/reject) per user preference.
- [2026-02-22T19:41:00Z] [codex-discord-presence-task-20260222T194048Z-d7k2|codex-discord-presence-task] created `TASK-109` for optional Discord Rich Presence integration with polished activity card UI; no code/runtime changes in this pass.
- [2026-02-22T19:46:00Z] [codex-task108-aniskip-20260222T194600Z-qgdt|codex-task108-aniskip] starting TASK-108 closure pass via Backlog MCP + writing-plans/executing-plans; validating plugin/docs behavior and finalizing AC/DoD evidence.
- [2026-02-22T19:49:30Z] [codex-task108-aniskip-20260222T194600Z-qgdt|codex-task108-aniskip] completed TASK-108 closure: wrote plan artifact, revalidated plugin/docs AC coverage, ran validation suite (`luac` + launcher tests pass), finalized backlog task as Done; `tsc --noEmit` still fails on unrelated pre-existing `src/anki-integration/note-update-workflow.test.ts` errors.
- [2026-02-22T19:47:35Z] [opencode-task105-unsafe-casts-20260222T194704Z-zfcm|opencode-task105-unsafe-casts] starting TASK-105 via Backlog MCP + writing-plans/executing-plans; scope runtime cast elimination in `src/main.ts` + `src/main/runtime/*`, no commit.
- [2026-02-22T19:47:08Z] [codex-task104-launcher-config-20260222T194708Z-z9x1|codex-task104-launcher-config] starting TASK-104 via Backlog MCP + writing-plans/executing-plans; scope launcher config module extraction + focused tests + launcher lanes validation.
- [2026-02-22T19:56:26Z] [codex-task104-launcher-config-20260222T194708Z-z9x1|codex-task104-launcher-config] completed TASK-104: split `launcher/config.ts` into domain modules + CLI builder/normalizer, added focused parser tests, aligned Jellyfin config reader contract (no token/userId fields), and verified `test:launcher` + `test:fast`; backlog task marked Done.
- [2026-02-22T19:51:09Z] [opencode-task106-immersion-modules-20260222T195109Z-r3m7|opencode-task106-immersion-modules] starting TASK-106 via Backlog MCP + writing-plans/executing-plans; scope immersion tracker storage/session/metadata decomposition + focused tests + architecture docs update, no commit.
- [2026-02-22T19:54:23Z] [codex-task105-sliceb-20260222T195423Z-w8n3|codex-task105-sliceb] overlap note: executing user-requested TASK-105 slice B in targeted runtime cast-removal files (`src/main/runtime/*-main-deps.ts`, jellyfin/mpv runtime helpers) while preserving existing TASK-105 planning artifacts.
- [2026-02-22T19:59:42Z] [codex-task105-sliceb-20260222T195423Z-w8n3|codex-task105-sliceb] completed requested slice B: removed unsafe casts in targeted runtime modules, tightened contracts with shared runtime types (`CliCommandContextFactoryDeps`, `TokenizerDepsRuntimeOptions`, overlay options deps), and verified via focused runtime test suite (16 pass, 0 fail).
- [2026-02-22T20:02:06Z] [codex-task105-sliceb-20260222T195423Z-w8n3|codex-task105-sliceb] follow-up typing fallout pass: updated focused runtime tests for stricter contract shapes (texthooker/anilist/mpv/jellyfin/tokenizer stubs) and revalidated same 8-file suite green.
- [2026-02-22T20:01:45Z] [opencode-task106-immersion-modules-20260222T195109Z-r3m7|opencode-task106-immersion-modules] completed TASK-106 implementation scope: extracted `storage.ts`, `session.ts`, `metadata.ts`; reduced `immersion-tracker-service.ts` to 654 LOC facade; added focused tests (`storage-session.test.ts`, `metadata.test.ts`); tracker/core source tests green. Task finalization blocked on AC#4 build gate due unrelated pre-existing TS errors in `src/anki-integration/*` and `src/main/runtime/*`.
- [2026-02-22T21:58:45Z] [opencode-task106-immersion-modules-20260222T195109Z-r3m7|opencode-task106-immersion-modules] blocker cleared; reran `bun run build` + tracker tests + `test:core:src` green; finalized TASK-106 to Done in Backlog with final summary.
- [2026-02-22T21:54:29Z] [codex-ts-build-errors-20260222T215411Z-h3k7|codex-ts-build-errors] overlap note: touching `src/main/runtime/*` and `src/anki-integration/note-update-workflow.test.ts` for strict-typing fallout fixes after TASK-105 slice-B contract tightening; scope limited to compile-error remediation + test stub alignment.
- [2026-02-22T21:55:54Z] [codex-ts-build-errors-20260222T215411Z-h3k7|codex-ts-build-errors] completed compile-fix pass: widened `note-update-workflow.test.ts` harness deps to `NoteUpdateWorkflowDeps`, aligned stub callback signatures, and verified `bun run tsc --noEmit` + `make build` green.
- [2026-02-22T21:56:30Z] [opencode-task105-unsafe-casts-20260222T194704Z-zfcm|opencode-task105-unsafe-casts] finalized TASK-105: re-ran `bun run build` after compile-fix pass (green), confirmed cast scan 42->0 in scope, and moved backlog task to Done with AC/DoD + final summary.

View File

@@ -514,7 +514,7 @@ let yomitanLoadInFlight: Promise<Extension | null> | null = null;
const buildApplyJellyfinMpvDefaultsMainDepsHandler = const buildApplyJellyfinMpvDefaultsMainDepsHandler =
createBuildApplyJellyfinMpvDefaultsMainDepsHandler({ createBuildApplyJellyfinMpvDefaultsMainDepsHandler({
sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command), sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client, command),
jellyfinLangPref: JELLYFIN_LANG_PREF, jellyfinLangPref: JELLYFIN_LANG_PREF,
}); });
const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler(); const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler();
@@ -522,7 +522,9 @@ const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler(
applyJellyfinMpvDefaultsMainDeps, applyJellyfinMpvDefaultsMainDeps,
); );
function applyJellyfinMpvDefaults(client: MpvIpcClient): void { function applyJellyfinMpvDefaults(
client: Parameters<typeof applyJellyfinMpvDefaultsHandler>[0],
): void {
applyJellyfinMpvDefaultsHandler(client); applyJellyfinMpvDefaultsHandler(client);
} }
@@ -718,36 +720,35 @@ const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsH
let appTray: Tray | null = null; let appTray: Tray | null = null;
const buildSubtitleProcessingControllerMainDepsHandler = const buildSubtitleProcessingControllerMainDepsHandler =
createBuildSubtitleProcessingControllerMainDepsHandler({ createBuildSubtitleProcessingControllerMainDepsHandler({
tokenizeSubtitle: async (text: string) => { tokenizeSubtitle: async (text: string) => {
if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) { if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) {
return null; return null;
} }
return await tokenizeSubtitle(text); return await tokenizeSubtitle(text);
}, },
emitSubtitle: (payload) => { emitSubtitle: (payload) => {
const previousSubtitleText = appState.currentSubtitleData?.text ?? null; const previousSubtitleText = appState.currentSubtitleData?.text ?? null;
const nextSubtitleText = payload?.text ?? null; const nextSubtitleText = payload?.text ?? null;
const subtitleChanged = previousSubtitleText !== nextSubtitleText; const subtitleChanged = previousSubtitleText !== nextSubtitleText;
appState.currentSubtitleData = payload; appState.currentSubtitleData = payload;
if (subtitleChanged) { if (subtitleChanged) {
appState.hoveredSubtitleTokenIndex = null; appState.hoveredSubtitleTokenIndex = null;
appState.hoveredSubtitleRevision += 1; appState.hoveredSubtitleRevision += 1;
applyHoveredTokenOverlay(); applyHoveredTokenOverlay();
} }
broadcastToOverlayWindows('subtitle:set', payload); broadcastToOverlayWindows('subtitle:set', payload);
subtitleWsService.broadcast(payload, { subtitleWsService.broadcast(payload, {
enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled,
topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX,
mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode,
}); });
}, },
logDebug: (message) => { logDebug: (message) => {
logger.debug(`[subtitle-processing] ${message}`); logger.debug(`[subtitle-processing] ${message}`);
}, },
now: () => Date.now(), now: () => Date.now(),
}); });
const subtitleProcessingControllerMainDeps = const subtitleProcessingControllerMainDeps = buildSubtitleProcessingControllerMainDepsHandler();
buildSubtitleProcessingControllerMainDepsHandler();
const subtitleProcessingController = createSubtitleProcessingController( const subtitleProcessingController = createSubtitleProcessingController(
subtitleProcessingControllerMainDeps, subtitleProcessingControllerMainDeps,
); );
@@ -811,20 +812,20 @@ const watchConfigPathHandler = createWatchConfigPathHandler(buildWatchConfigPath
const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler( const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler(
{ {
setKeybindings: (keybindings) => { setKeybindings: (keybindings) => {
appState.keybindings = keybindings as never; appState.keybindings = keybindings;
}, },
refreshGlobalAndOverlayShortcuts: () => { refreshGlobalAndOverlayShortcuts: () => {
refreshGlobalAndOverlayShortcuts(); refreshGlobalAndOverlayShortcuts();
}, },
setSecondarySubMode: (mode) => { setSecondarySubMode: (mode) => {
appState.secondarySubMode = mode as never; appState.secondarySubMode = mode;
}, },
broadcastToOverlayWindows: (channel, payload) => { broadcastToOverlayWindows: (channel, payload) => {
broadcastToOverlayWindows(channel, payload); broadcastToOverlayWindows(channel, payload);
}, },
applyAnkiRuntimeConfigPatch: (patch) => { applyAnkiRuntimeConfigPatch: (patch) => {
if (appState.ankiIntegration) { if (appState.ankiIntegration) {
appState.ankiIntegration.applyRuntimeConfigPatch(patch as never); appState.ankiIntegration.applyRuntimeConfigPatch(patch);
} }
}, },
}, },
@@ -912,7 +913,7 @@ const jlptDictionaryRuntime = createJlptDictionaryRuntimeService(
getDictionaryRoots: () => buildDictionaryRootsHandler(), getDictionaryRoots: () => buildDictionaryRootsHandler(),
getJlptDictionarySearchPaths, getJlptDictionarySearchPaths,
setJlptLevelLookup: (lookup) => { setJlptLevelLookup: (lookup) => {
appState.jlptLevelLookup = lookup as never; appState.jlptLevelLookup = lookup;
}, },
logInfo: (message) => logger.info(message), logInfo: (message) => logger.info(message),
})(), })(),
@@ -926,7 +927,7 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService(
getFrequencyDictionarySearchPaths, getFrequencyDictionarySearchPaths,
getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath, getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath,
setFrequencyRankLookup: (lookup) => { setFrequencyRankLookup: (lookup) => {
appState.frequencyRankLookup = lookup as never; appState.frequencyRankLookup = lookup;
}, },
logInfo: (message) => logger.info(message), logInfo: (message) => logger.info(message),
})(), })(),
@@ -968,7 +969,7 @@ function setFieldGroupingResolver(
} }
const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>( const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime<OverlayHostedModal>(
createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal, KikuFieldGroupingChoice>({ createBuildFieldGroupingOverlayMainDepsHandler<OverlayHostedModal>({
getMainWindow: () => overlayManager.getMainWindow(), getMainWindow: () => overlayManager.getMainWindow(),
getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(), getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(),
@@ -1257,8 +1258,7 @@ const buildPlayJellyfinItemInMpvMainDepsHandler = createBuildPlayJellyfinItemInM
subtitleStreamIndex: params.subtitleStreamIndex ?? undefined, subtitleStreamIndex: params.subtitleStreamIndex ?? undefined,
}, },
), ),
applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient),
applyJellyfinMpvDefaults(mpvClient as unknown as MpvIpcClient),
sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command), sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command),
armQuitOnDisconnect: () => { armQuitOnDisconnect: () => {
jellyfinPlayQuitOnDisconnectArmed = false; jellyfinPlayQuitOnDisconnectArmed = false;
@@ -2169,10 +2169,7 @@ const {
}, },
}, },
mpvClientRuntimeServiceFactoryMainDeps: { mpvClientRuntimeServiceFactoryMainDeps: {
createClient: MpvIpcClient as unknown as new ( createClient: MpvIpcClient,
socketPath: string,
options: MpvClientRuntimeServiceOptions,
) => MpvIpcClient,
getSocketPath: () => appState.mpvSocketPath, getSocketPath: () => appState.mpvSocketPath,
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
isAutoStartOverlayEnabled: () => appState.autoStartOverlay, isAutoStartOverlayEnabled: () => appState.autoStartOverlay,
@@ -2434,7 +2431,7 @@ const { appendToMpvLog, flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers(
buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({ buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({
appendToMpvLog: (message) => appendToMpvLogHandler(message), appendToMpvLog: (message) => appendToMpvLogHandler(message),
showMpvOsdRuntime: (mpvClient, text, fallbackLog) => showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
showMpvOsdRuntime(mpvClient as never, text, fallbackLog), showMpvOsdRuntime(mpvClient, text, fallbackLog),
getMpvClient: () => appState.mpvClient, getMpvClient: () => appState.mpvClient,
logInfo: (line) => logger.info(line), logInfo: (line) => logger.info(line),
}), }),
@@ -2845,7 +2842,7 @@ const {
}, },
createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath), createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath),
createEmptyImage: () => nativeImage.createEmpty(), createEmptyImage: () => nativeImage.createEmpty(),
createTray: (icon) => new Tray(icon as never), createTray: (icon) => new Tray(icon as ConstructorParameters<typeof Tray>[0]),
trayTooltip: TRAY_TOOLTIP, trayTooltip: TRAY_TOOLTIP,
platform: process.platform, platform: process.platform,
logWarn: (message) => logger.warn(message), logWarn: (message) => logger.warn(message),
@@ -2910,12 +2907,12 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } =
getOverlayWindows: () => getOverlayWindows(), getOverlayWindows: () => getOverlayWindows(),
getResolvedConfig: () => getResolvedConfig(), getResolvedConfig: () => getResolvedConfig(),
showDesktopNotification, showDesktopNotification,
createFieldGroupingCallback: () => createFieldGroupingCallback() as never, createFieldGroupingCallback: () => createFieldGroupingCallback(),
getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'), getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'),
}, },
initializeOverlayRuntimeBootstrapDeps: { initializeOverlayRuntimeBootstrapDeps: {
isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized,
initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options as never), initializeOverlayRuntimeCore,
setInvisibleOverlayVisible: (visible) => { setInvisibleOverlayVisible: (visible) => {
overlayManager.setInvisibleOverlayVisible(visible); overlayManager.setInvisibleOverlayVisible(visible);
}, },

View File

@@ -1,11 +1,11 @@
export function createBuildEnsureTrayMainDepsHandler(deps: { export function createBuildEnsureTrayMainDepsHandler<TTray, TTrayMenu, TTrayIcon>(deps: {
getTray: () => unknown | null; getTray: () => TTray | null;
setTray: (tray: unknown | null) => void; setTray: (tray: TTray | null) => void;
buildTrayMenu: () => unknown; buildTrayMenu: () => TTrayMenu;
resolveTrayIconPath: () => string | null; resolveTrayIconPath: () => string | null;
createImageFromPath: (iconPath: string) => unknown; createImageFromPath: (iconPath: string) => TTrayIcon;
createEmptyImage: () => unknown; createEmptyImage: () => TTrayIcon;
createTray: (icon: unknown) => unknown; createTray: (icon: TTrayIcon) => TTray;
trayTooltip: string; trayTooltip: string;
platform: string; platform: string;
logWarn: (message: string) => void; logWarn: (message: string) => void;
@@ -14,13 +14,13 @@ export function createBuildEnsureTrayMainDepsHandler(deps: {
setVisibleOverlayVisible: (visible: boolean) => void; setVisibleOverlayVisible: (visible: boolean) => void;
}) { }) {
return () => ({ return () => ({
getTray: () => deps.getTray() as never, getTray: () => deps.getTray(),
setTray: (tray: unknown | null) => deps.setTray(tray), setTray: (tray: TTray | null) => deps.setTray(tray),
buildTrayMenu: () => deps.buildTrayMenu() as never, buildTrayMenu: () => deps.buildTrayMenu(),
resolveTrayIconPath: () => deps.resolveTrayIconPath(), resolveTrayIconPath: () => deps.resolveTrayIconPath(),
createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath) as never, createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath),
createEmptyImage: () => deps.createEmptyImage() as never, createEmptyImage: () => deps.createEmptyImage(),
createTray: (icon: unknown) => deps.createTray(icon) as never, createTray: (icon: TTrayIcon) => deps.createTray(icon),
trayTooltip: deps.trayTooltip, trayTooltip: deps.trayTooltip,
platform: deps.platform, platform: deps.platform,
logWarn: (message: string) => deps.logWarn(message), logWarn: (message: string) => deps.logWarn(message),
@@ -33,28 +33,28 @@ export function createBuildEnsureTrayMainDepsHandler(deps: {
}); });
} }
export function createBuildDestroyTrayMainDepsHandler(deps: { export function createBuildDestroyTrayMainDepsHandler<TTray>(deps: {
getTray: () => unknown | null; getTray: () => TTray | null;
setTray: (tray: unknown | null) => void; setTray: (tray: TTray | null) => void;
}) { }) {
return () => ({ return () => ({
getTray: () => deps.getTray() as never, getTray: () => deps.getTray(),
setTray: (tray: unknown | null) => deps.setTray(tray), setTray: (tray: TTray | null) => deps.setTray(tray),
}); });
} }
export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps: { export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<TOptions>(deps: {
isOverlayRuntimeInitialized: () => boolean; isOverlayRuntimeInitialized: () => boolean;
initializeOverlayRuntimeCore: (options: unknown) => { invisibleOverlayVisible: boolean }; initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean };
buildOptions: () => unknown; buildOptions: () => TOptions;
setInvisibleOverlayVisible: (visible: boolean) => void; setInvisibleOverlayVisible: (visible: boolean) => void;
setOverlayRuntimeInitialized: (initialized: boolean) => void; setOverlayRuntimeInitialized: (initialized: boolean) => void;
startBackgroundWarmups: () => void; startBackgroundWarmups: () => void;
}) { }) {
return () => ({ return () => ({
isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(), isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(),
initializeOverlayRuntimeCore: (options: unknown) => deps.initializeOverlayRuntimeCore(options), initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options),
buildOptions: () => deps.buildOptions() as never, buildOptions: () => deps.buildOptions(),
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
setOverlayRuntimeInitialized: (initialized: boolean) => setOverlayRuntimeInitialized: (initialized: boolean) =>
deps.setOverlayRuntimeInitialized(initialized), deps.setOverlayRuntimeInitialized(initialized),
@@ -62,27 +62,27 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps
}); });
} }
export function createBuildOpenYomitanSettingsMainDepsHandler(deps: { export function createBuildOpenYomitanSettingsMainDepsHandler<TYomitanExt, TWindow>(deps: {
ensureYomitanExtensionLoaded: () => Promise<unknown | null>; ensureYomitanExtensionLoaded: () => Promise<TYomitanExt | null>;
openYomitanSettingsWindow: (params: { openYomitanSettingsWindow: (params: {
yomitanExt: unknown; yomitanExt: TYomitanExt;
getExistingWindow: () => unknown | null; getExistingWindow: () => TWindow | null;
setWindow: (window: unknown | null) => void; setWindow: (window: TWindow | null) => void;
}) => void; }) => void;
getExistingWindow: () => unknown | null; getExistingWindow: () => TWindow | null;
setWindow: (window: unknown | null) => void; setWindow: (window: TWindow | null) => void;
logWarn: (message: string) => void; logWarn: (message: string) => void;
logError: (message: string, error: unknown) => void; logError: (message: string, error: unknown) => void;
}) { }) {
return () => ({ return () => ({
ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(), ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(),
openYomitanSettingsWindow: (params: { openYomitanSettingsWindow: (params: {
yomitanExt: unknown; yomitanExt: TYomitanExt;
getExistingWindow: () => unknown | null; getExistingWindow: () => TWindow | null;
setWindow: (window: unknown | null) => void; setWindow: (window: TWindow | null) => void;
}) => deps.openYomitanSettingsWindow(params), }) => deps.openYomitanSettingsWindow(params),
getExistingWindow: () => deps.getExistingWindow(), getExistingWindow: () => deps.getExistingWindow(),
setWindow: (window: unknown | null) => deps.setWindow(window), setWindow: (window: TWindow | null) => deps.setWindow(window),
logWarn: (message: string) => deps.logWarn(message), logWarn: (message: string) => deps.logWarn(message),
logError: (message: string, error: unknown) => deps.logError(message, error), logError: (message: string, error: unknown) => deps.logError(message, error),
}); });

View File

@@ -6,14 +6,14 @@ test('cli command context factory composes main deps and context handlers', () =
const calls: string[] = []; const calls: string[] = [];
const appState = { const appState = {
mpvSocketPath: '/tmp/mpv.sock', mpvSocketPath: '/tmp/mpv.sock',
mpvClient: null as unknown, mpvClient: null,
texthookerPort: 5174, texthookerPort: 5174,
overlayRuntimeInitialized: false, overlayRuntimeInitialized: false,
}; };
const createContext = createCliCommandContextFactory({ const createContext = createCliCommandContextFactory({
appState, appState,
texthookerService: { start: () => null }, texthookerService: { isRunning: () => false, start: () => null },
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }), getResolvedConfig: () => ({ texthooker: { openBrowser: true } }),
openExternal: async () => {}, openExternal: async () => {},
logBrowserOpenError: () => {}, logBrowserOpenError: () => {},
@@ -32,11 +32,28 @@ test('cli command context factory composes main deps and context handlers', () =
triggerFieldGrouping: async () => {}, triggerFieldGrouping: async () => {},
triggerSubsyncFromConfig: async () => {}, triggerSubsyncFromConfig: async () => {},
markLastCardAsAudioCard: async () => {}, markLastCardAsAudioCard: async () => {},
getAnilistStatus: () => ({ status: 'ok' }), getAnilistStatus: () => ({
tokenStatus: 'resolved',
tokenSource: 'literal',
tokenMessage: null,
tokenResolvedAt: null,
tokenErrorAt: null,
queuePending: 0,
queueReady: 0,
queueDeadLetter: 0,
queueLastAttemptAt: null,
queueLastError: null,
}),
clearAnilistToken: () => {}, clearAnilistToken: () => {},
openAnilistSetupWindow: () => {}, openAnilistSetupWindow: () => {},
openJellyfinSetupWindow: () => {}, openJellyfinSetupWindow: () => {},
getAnilistQueueStatus: () => ({ queued: 0 }), getAnilistQueueStatus: () => ({
pending: 0,
ready: 0,
deadLetter: 0,
lastAttemptAt: null,
lastError: null,
}),
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }), processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
runJellyfinCommand: async () => {}, runJellyfinCommand: async () => {},
openYomitanSettings: () => {}, openYomitanSettings: () => {},

View File

@@ -6,14 +6,14 @@ test('cli command context main deps builder maps state and callbacks', async ()
const calls: string[] = []; const calls: string[] = [];
const appState = { const appState = {
mpvSocketPath: '/tmp/mpv.sock', mpvSocketPath: '/tmp/mpv.sock',
mpvClient: null as unknown, mpvClient: null,
texthookerPort: 5174, texthookerPort: 5174,
overlayRuntimeInitialized: false, overlayRuntimeInitialized: false,
}; };
const build = createBuildCliCommandContextMainDepsHandler({ const build = createBuildCliCommandContextMainDepsHandler({
appState, appState,
texthookerService: { start: () => null }, texthookerService: { isRunning: () => false, start: () => null },
getResolvedConfig: () => ({ texthooker: { openBrowser: true } }), getResolvedConfig: () => ({ texthooker: { openBrowser: true } }),
openExternal: async (url) => { openExternal: async (url) => {
calls.push(`open:${url}`); calls.push(`open:${url}`);
@@ -49,11 +49,28 @@ test('cli command context main deps builder maps state and callbacks', async ()
calls.push('mark-audio'); calls.push('mark-audio');
}, },
getAnilistStatus: () => ({ status: 'ok' }), getAnilistStatus: () => ({
tokenStatus: 'resolved',
tokenSource: 'literal',
tokenMessage: null,
tokenResolvedAt: null,
tokenErrorAt: null,
queuePending: 0,
queueReady: 0,
queueDeadLetter: 0,
queueLastAttemptAt: null,
queueLastError: null,
}),
clearAnilistToken: () => calls.push('clear-token'), clearAnilistToken: () => calls.push('clear-token'),
openAnilistSetupWindow: () => calls.push('open-anilist-setup'), openAnilistSetupWindow: () => calls.push('open-anilist-setup'),
openJellyfinSetupWindow: () => calls.push('open-jellyfin-setup'), openJellyfinSetupWindow: () => calls.push('open-jellyfin-setup'),
getAnilistQueueStatus: () => ({ queued: 1 }), getAnilistQueueStatus: () => ({
pending: 1,
ready: 0,
deadLetter: 0,
lastAttemptAt: null,
lastError: null,
}),
processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }), processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }),
runJellyfinCommand: async () => { runJellyfinCommand: async () => {
calls.push('run-jellyfin'); calls.push('run-jellyfin');

View File

@@ -1,13 +1,16 @@
import type { CliArgs } from '../../cli/args'; import type { CliArgs } from '../../cli/args';
import type { CliCommandContextFactoryDeps } from './cli-command-context';
type CliCommandContextMainState = {
mpvSocketPath: string;
mpvClient: ReturnType<CliCommandContextFactoryDeps['getMpvClient']>;
texthookerPort: number;
overlayRuntimeInitialized: boolean;
};
export function createBuildCliCommandContextMainDepsHandler(deps: { export function createBuildCliCommandContextMainDepsHandler(deps: {
appState: { appState: CliCommandContextMainState;
mpvSocketPath: string; texthookerService: CliCommandContextFactoryDeps['texthookerService'];
mpvClient: unknown | null;
texthookerPort: number;
overlayRuntimeInitialized: boolean;
};
texthookerService: unknown;
getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } }; getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } };
openExternal: (url: string) => Promise<unknown>; openExternal: (url: string) => Promise<unknown>;
logBrowserOpenError: (url: string, error: unknown) => void; logBrowserOpenError: (url: string, error: unknown) => void;
@@ -29,12 +32,12 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
triggerSubsyncFromConfig: () => Promise<void>; triggerSubsyncFromConfig: () => Promise<void>;
markLastCardAsAudioCard: () => Promise<void>; markLastCardAsAudioCard: () => Promise<void>;
getAnilistStatus: () => unknown; getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus'];
clearAnilistToken: () => void; clearAnilistToken: () => void;
openAnilistSetupWindow: () => void; openAnilistSetupWindow: () => void;
openJellyfinSetupWindow: () => void; openJellyfinSetupWindow: () => void;
getAnilistQueueStatus: () => unknown; getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus'];
processNextAnilistRetryUpdate: () => Promise<{ ok: boolean; message: string }>; processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow'];
runJellyfinCommand: (args: CliArgs) => Promise<void>; runJellyfinCommand: (args: CliArgs) => Promise<void>;
openYomitanSettings: () => void; openYomitanSettings: () => void;
@@ -49,14 +52,14 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
logWarn: (message: string) => void; logWarn: (message: string) => void;
logError: (message: string, err: unknown) => void; logError: (message: string, err: unknown) => void;
}) { }) {
return () => ({ return (): CliCommandContextFactoryDeps => ({
getSocketPath: () => deps.appState.mpvSocketPath, getSocketPath: () => deps.appState.mpvSocketPath,
setSocketPath: (socketPath: string) => { setSocketPath: (socketPath: string) => {
deps.appState.mpvSocketPath = socketPath; deps.appState.mpvSocketPath = socketPath;
}, },
getMpvClient: () => deps.appState.mpvClient as never, getMpvClient: () => deps.appState.mpvClient,
showOsd: (text: string) => deps.showMpvOsd(text), showOsd: (text: string) => deps.showMpvOsd(text),
texthookerService: deps.texthookerService as never, texthookerService: deps.texthookerService,
getTexthookerPort: () => deps.appState.texthookerPort, getTexthookerPort: () => deps.appState.texthookerPort,
setTexthookerPort: (port: number) => { setTexthookerPort: (port: number) => {
deps.appState.texthookerPort = port; deps.appState.texthookerPort = port;
@@ -80,11 +83,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: {
triggerFieldGrouping: () => deps.triggerFieldGrouping(), triggerFieldGrouping: () => deps.triggerFieldGrouping(),
triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(), triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(),
markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(), markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(),
getAnilistStatus: () => deps.getAnilistStatus() as never, getAnilistStatus: () => deps.getAnilistStatus(),
clearAnilistToken: () => deps.clearAnilistToken(), clearAnilistToken: () => deps.clearAnilistToken(),
openAnilistSetup: () => deps.openAnilistSetupWindow(), openAnilistSetup: () => deps.openAnilistSetupWindow(),
openJellyfinSetup: () => deps.openJellyfinSetupWindow(), openJellyfinSetup: () => deps.openJellyfinSetupWindow(),
getAnilistQueueStatus: () => deps.getAnilistQueueStatus() as never, getAnilistQueueStatus: () => deps.getAnilistQueueStatus(),
retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(), retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(),
runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args), runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args),
openYomitanSettings: () => deps.openYomitanSettings(), openYomitanSettings: () => deps.openYomitanSettings(),

View File

@@ -5,7 +5,7 @@ import { composeAppReadyRuntime } from './app-ready-composer';
test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => { test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => {
const composed = composeAppReadyRuntime({ const composed = composeAppReadyRuntime({
reloadConfigMainDeps: { reloadConfigMainDeps: {
reloadConfigStrict: () => ({ config: {} as never, warnings: [] }), reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
logInfo: () => {}, logInfo: () => {},
logWarning: () => {}, logWarning: () => {},
showDesktopNotification: () => {}, showDesktopNotification: () => {},

View File

@@ -85,7 +85,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
mpvClientRuntimeServiceFactoryMainDeps: { mpvClientRuntimeServiceFactoryMainDeps: {
createClient: FakeMpvClient, createClient: FakeMpvClient,
getSocketPath: () => '/tmp/mpv.sock', getSocketPath: () => '/tmp/mpv.sock',
getResolvedConfig: () => ({}) as never, getResolvedConfig: () => ({ auto_start_overlay: false }),
isAutoStartOverlayEnabled: () => true, isAutoStartOverlayEnabled: () => true,
setOverlayVisible: () => {}, setOverlayVisible: () => {},
shouldBindVisibleOverlayToMpvSubVisibility: () => true, shouldBindVisibleOverlayToMpvSubVisibility: () => true,
@@ -118,7 +118,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject
setYomitanParserInitPromise: () => {}, setYomitanParserInitPromise: () => {},
isKnownWord: (text) => text === 'known', isKnownWord: (text) => text === 'known',
recordLookup: () => {}, recordLookup: () => {},
getKnownWordMatchMode: () => 'exact', getKnownWordMatchMode: () => 'headword',
getMinSentenceWordsForNPlusOne: () => 3, getMinSentenceWordsForNPlusOne: () => 3,
getJlptLevel: () => null, getJlptLevel: () => null,
getJlptEnabled: () => true, getJlptEnabled: () => true,

View File

@@ -3,6 +3,7 @@ import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main-
import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps'; import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps';
import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service'; import { createMpvClientRuntimeServiceFactory } from '../mpv-client-runtime-service';
import type { MpvClientRuntimeServiceOptions } from '../mpv-client-runtime-service'; import type { MpvClientRuntimeServiceOptions } from '../mpv-client-runtime-service';
import type { Config } from '../../../types';
import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps'; import { createBuildUpdateMpvSubtitleRenderMetricsMainDepsHandler } from '../mpv-subtitle-render-metrics-main-deps';
import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics'; import { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics';
import { import {
@@ -30,7 +31,7 @@ type MpvClientRuntimeServiceFactoryMainDeps<TMpvClient extends RuntimeMpvClient>
Parameters< Parameters<
typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler< typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler<
TMpvClient, TMpvClient,
unknown, Config,
MpvClientRuntimeServiceOptions MpvClientRuntimeServiceOptions
> >
>[0], >[0],
@@ -107,7 +108,7 @@ export function composeMpvRuntimeHandlers<
const buildMpvClientRuntimeServiceFactoryMainDepsHandler = const buildMpvClientRuntimeServiceFactoryMainDepsHandler =
createBuildMpvClientRuntimeServiceFactoryDepsHandler< createBuildMpvClientRuntimeServiceFactoryDepsHandler<
TMpvClient, TMpvClient,
unknown, Config,
MpvClientRuntimeServiceOptions MpvClientRuntimeServiceOptions
>({ >({
...options.mpvClientRuntimeServiceFactoryMainDeps, ...options.mpvClientRuntimeServiceFactoryMainDeps,

View File

@@ -1,3 +1,7 @@
import type { FrequencyDictionaryLookup, JlptLevel } from '../../types';
type JlptLookup = (term: string) => JlptLevel | null;
export function createBuildDictionaryRootsMainHandler(deps: { export function createBuildDictionaryRootsMainHandler(deps: {
dirname: string; dirname: string;
appPath: string; appPath: string;
@@ -8,20 +12,19 @@ export function createBuildDictionaryRootsMainHandler(deps: {
cwd: string; cwd: string;
joinPath: (...parts: string[]) => string; joinPath: (...parts: string[]) => string;
}) { }) {
return () => return () => [
[ deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'),
deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'), deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'),
deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'), deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'),
deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'), deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'),
deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'), deps.userDataPath,
deps.userDataPath, deps.appUserDataPath,
deps.appUserDataPath, deps.joinPath(deps.homeDir, '.config', 'SubMiner'),
deps.joinPath(deps.homeDir, '.config', 'SubMiner'), deps.joinPath(deps.homeDir, '.config', 'subminer'),
deps.joinPath(deps.homeDir, '.config', 'subminer'), deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'),
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'SubMiner'), deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'),
deps.joinPath(deps.homeDir, 'Library', 'Application Support', 'subminer'), deps.cwd,
deps.cwd, ];
];
} }
export function createBuildFrequencyDictionaryRootsMainHandler(deps: { export function createBuildFrequencyDictionaryRootsMainHandler(deps: {
@@ -57,7 +60,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
isJlptEnabled: () => boolean; isJlptEnabled: () => boolean;
getDictionaryRoots: () => string[]; getDictionaryRoots: () => string[];
getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[]; getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[];
setJlptLevelLookup: (lookup: unknown) => void; setJlptLevelLookup: (lookup: JlptLookup) => void;
logInfo: (message: string) => void; logInfo: (message: string) => void;
}) { }) {
return () => ({ return () => ({
@@ -66,7 +69,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: {
deps.getJlptDictionarySearchPaths({ deps.getJlptDictionarySearchPaths({
getDictionaryRoots: () => deps.getDictionaryRoots(), getDictionaryRoots: () => deps.getDictionaryRoots(),
}), }),
setJlptLevelLookup: (lookup: unknown) => deps.setJlptLevelLookup(lookup), setJlptLevelLookup: (lookup: JlptLookup) => deps.setJlptLevelLookup(lookup),
log: (message: string) => deps.logInfo(`[JLPT] ${message}`), log: (message: string) => deps.logInfo(`[JLPT] ${message}`),
}); });
} }
@@ -79,17 +82,19 @@ export function createBuildFrequencyDictionaryRuntimeMainDepsHandler(deps: {
getSourcePath: () => string | undefined; getSourcePath: () => string | undefined;
}) => string[]; }) => string[];
getSourcePath: () => string | undefined; getSourcePath: () => string | undefined;
setFrequencyRankLookup: (lookup: unknown) => void; setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => void;
logInfo: (message: string) => void; logInfo: (message: string) => void;
}) { }) {
return () => ({ return () => ({
isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(), isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(),
getSearchPaths: () => getSearchPaths: () =>
deps.getFrequencyDictionarySearchPaths({ deps.getFrequencyDictionarySearchPaths({
getDictionaryRoots: () => deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot), getDictionaryRoots: () =>
deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot),
getSourcePath: () => deps.getSourcePath(), getSourcePath: () => deps.getSourcePath(),
}), }),
setFrequencyRankLookup: (lookup: unknown) => deps.setFrequencyRankLookup(lookup), setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) =>
deps.setFrequencyRankLookup(lookup),
log: (message: string) => deps.logInfo(`[Frequency] ${message}`), log: (message: string) => deps.logInfo(`[Frequency] ${message}`),
}); });
} }

View File

@@ -8,7 +8,12 @@ test('field grouping overlay main deps builder maps window visibility and resolv
const resolver = (choice: unknown) => calls.push(`resolver:${choice}`); const resolver = (choice: unknown) => calls.push(`resolver:${choice}`);
const deps = createBuildFieldGroupingOverlayMainDepsHandler({ const deps = createBuildFieldGroupingOverlayMainDepsHandler({
getMainWindow: () => ({ id: 'main' }), getMainWindow: () => ({
isDestroyed: () => false,
webContents: {
send: () => {},
},
}),
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false, getInvisibleOverlayVisible: () => false,
setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`), setVisibleOverlayVisible: (visible) => calls.push(`visible:${visible}`),
@@ -24,7 +29,7 @@ test('field grouping overlay main deps builder maps window visibility and resolv
}, },
})(); })();
assert.deepEqual(deps.getMainWindow(), { id: 'main' }); assert.equal(deps.getMainWindow()?.isDestroyed(), false);
assert.equal(deps.getVisibleOverlayVisible(), true); assert.equal(deps.getVisibleOverlayVisible(), true);
assert.equal(deps.getInvisibleOverlayVisible(), false); assert.equal(deps.getInvisibleOverlayVisible(), false);
assert.equal(deps.getResolver(), resolver); assert.equal(deps.getResolver(), resolver);

View File

@@ -1,29 +1,34 @@
export function createBuildFieldGroupingOverlayMainDepsHandler< import type { FieldGroupingOverlayRuntimeOptions } from '../../core/services/field-grouping-overlay';
TModal extends string,
TChoice, type FieldGroupingOverlayMainDeps<TModal extends string> = Omit<
>(deps: { FieldGroupingOverlayRuntimeOptions<TModal>,
getMainWindow: () => unknown | null; 'sendToVisibleOverlay'
getVisibleOverlayVisible: () => boolean; > & {
getInvisibleOverlayVisible: () => boolean;
setVisibleOverlayVisible: (visible: boolean) => void;
setInvisibleOverlayVisible: (visible: boolean) => void;
getResolver: () => ((choice: TChoice) => void) | null;
setResolver: (resolver: ((choice: TChoice) => void) | null) => void;
getRestoreVisibleOverlayOnModalClose: () => Set<TModal>;
sendToActiveOverlayWindow: ( sendToActiveOverlayWindow: (
channel: string, channel: string,
payload?: unknown, payload?: unknown,
runtimeOptions?: { restoreOnModalClose?: TModal }, runtimeOptions?: { restoreOnModalClose?: TModal },
) => boolean; ) => boolean;
}) { };
return () => ({
getMainWindow: () => deps.getMainWindow() as never, type BuiltFieldGroupingOverlayMainDeps<TModal extends string> =
FieldGroupingOverlayRuntimeOptions<TModal> & {
sendToVisibleOverlay: NonNullable<
FieldGroupingOverlayRuntimeOptions<TModal>['sendToVisibleOverlay']
>;
};
export function createBuildFieldGroupingOverlayMainDepsHandler<TModal extends string>(
deps: FieldGroupingOverlayMainDeps<TModal>,
) {
return (): BuiltFieldGroupingOverlayMainDeps<TModal> => ({
getMainWindow: () => deps.getMainWindow(),
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible),
setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible),
getResolver: () => deps.getResolver() as never, getResolver: () => deps.getResolver(),
setResolver: (resolver: ((choice: TChoice) => void) | null) => deps.setResolver(resolver), setResolver: (resolver) => deps.setResolver(resolver),
getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(), getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(),
sendToVisibleOverlay: ( sendToVisibleOverlay: (
channel: string, channel: string,

View File

@@ -1,13 +1,19 @@
import type { JellyfinStoredSession } from '../../core/services/jellyfin-token-store';
import type { ResolvedConfig } from '../../types';
type ResolvedJellyfinConfig = ResolvedConfig['jellyfin'];
type ResolvedJellyfinConfigWithSession = ResolvedJellyfinConfig & {
accessToken?: string;
userId?: string;
};
export function createGetResolvedJellyfinConfigHandler(deps: { export function createGetResolvedJellyfinConfigHandler(deps: {
getResolvedConfig: () => { jellyfin: unknown }; getResolvedConfig: () => { jellyfin: ResolvedJellyfinConfig };
loadStoredSession: () => { accessToken: string; userId: string } | null | undefined; loadStoredSession: () => JellyfinStoredSession | null | undefined;
getEnv: (name: string) => string | undefined; getEnv: (name: string) => string | undefined;
}) { }) {
return () => { return (): ResolvedJellyfinConfigWithSession => {
const jellyfin = deps.getResolvedConfig().jellyfin as { const jellyfin = deps.getResolvedConfig().jellyfin;
userId?: string;
[key: string]: unknown;
};
const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? ''; const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? '';
const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? ''; const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? '';
@@ -20,7 +26,7 @@ export function createGetResolvedJellyfinConfigHandler(deps: {
...jellyfin, ...jellyfin,
accessToken: envToken, accessToken: envToken,
userId: envUserId || storedUserId || '', userId: envUserId || storedUserId || '',
} as never; };
} }
if (storedToken.length > 0 && storedUserId.length > 0) { if (storedToken.length > 0 && storedUserId.length > 0) {
@@ -28,24 +34,20 @@ export function createGetResolvedJellyfinConfigHandler(deps: {
...jellyfin, ...jellyfin,
accessToken: storedToken, accessToken: storedToken,
userId: storedUserId, userId: storedUserId,
} as never; };
} }
return jellyfin as never; return jellyfin;
}; };
} }
export function createGetJellyfinClientInfoHandler(deps: { export function createGetJellyfinClientInfoHandler(deps: {
getResolvedJellyfinConfig: () => { getResolvedJellyfinConfig: () => Partial<
clientName?: string; Pick<ResolvedJellyfinConfig, 'clientName' | 'clientVersion' | 'deviceId'>
clientVersion?: string; >;
deviceId?: string; getDefaultJellyfinConfig: () => Partial<
}; Pick<ResolvedJellyfinConfig, 'clientName' | 'clientVersion' | 'deviceId'>
getDefaultJellyfinConfig: () => { >;
clientName?: string;
clientVersion?: string;
deviceId?: string;
};
}) { }) {
return ( return (
config = deps.getResolvedJellyfinConfig(), config = deps.getResolvedJellyfinConfig(),

View File

@@ -6,12 +6,14 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => {
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildPlayJellyfinItemInMpvMainDepsHandler({ const deps = createBuildPlayJellyfinItemInMpvMainDepsHandler({
ensureMpvConnectedForPlayback: async () => true, ensureMpvConnectedForPlayback: async () => true,
getMpvClient: () => ({ connected: true }), getMpvClient: () => ({ connected: true, send: () => {} }),
resolvePlaybackPlan: async () => ({ resolvePlaybackPlan: async () => ({
url: 'u', url: 'u',
mode: 'direct', mode: 'direct',
title: 't', title: 't',
startTimeTicks: 0, startTimeTicks: 0,
audioStreamIndex: null,
subtitleStreamIndex: null,
}), }),
applyJellyfinMpvDefaults: () => calls.push('defaults'), applyJellyfinMpvDefaults: () => calls.push('defaults'),
sendMpvCommand: (command) => calls.push(`cmd:${command[0]}`), sendMpvCommand: (command) => calls.push(`cmd:${command[0]}`),
@@ -28,18 +30,57 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => {
assert.equal(await deps.ensureMpvConnectedForPlayback(), true); assert.equal(await deps.ensureMpvConnectedForPlayback(), true);
assert.equal(typeof deps.getMpvClient(), 'object'); assert.equal(typeof deps.getMpvClient(), 'object');
assert.deepEqual( assert.deepEqual(
await deps.resolvePlaybackPlan({ session: {} as never, clientInfo: {} as never, jellyfinConfig: {}, itemId: 'i' }), await deps.resolvePlaybackPlan({
{ url: 'u', mode: 'direct', title: 't', startTimeTicks: 0 }, session: {
serverUrl: 'http://localhost:8096',
accessToken: 'token',
userId: 'uid',
username: 'alice',
},
clientInfo: {
clientName: 'SubMiner',
clientVersion: '1.0.0',
deviceId: 'did',
},
jellyfinConfig: {},
itemId: 'i',
}),
{
url: 'u',
mode: 'direct',
title: 't',
startTimeTicks: 0,
audioStreamIndex: null,
subtitleStreamIndex: null,
},
); );
deps.applyJellyfinMpvDefaults({}); deps.applyJellyfinMpvDefaults({ connected: true, send: () => {} });
deps.sendMpvCommand(['show-text', 'x']); deps.sendMpvCommand(['show-text', 'x']);
deps.armQuitOnDisconnect(); deps.armQuitOnDisconnect();
deps.schedule(() => {}, 500); deps.schedule(() => {}, 500);
assert.equal(deps.convertTicksToSeconds(20_000_000), 2); assert.equal(deps.convertTicksToSeconds(20_000_000), 2);
deps.preloadExternalSubtitles({ session: {} as never, clientInfo: {} as never, itemId: 'i' }); deps.preloadExternalSubtitles({
session: {
serverUrl: 'http://localhost:8096',
accessToken: 'token',
userId: 'uid',
username: 'alice',
},
clientInfo: {
clientName: 'SubMiner',
clientVersion: '1.0.0',
deviceId: 'did',
},
itemId: 'i',
});
deps.setActivePlayback({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay' }); deps.setActivePlayback({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay' });
deps.setLastProgressAtMs(0); deps.setLastProgressAtMs(0);
deps.reportPlaying({ itemId: 'i', mediaSourceId: undefined, playMethod: 'DirectPlay', eventName: 'start' }); deps.reportPlaying({
itemId: 'i',
mediaSourceId: undefined,
playMethod: 'DirectPlay',
eventName: 'start',
});
deps.showMpvOsd('ok'); deps.showMpvOsd('ok');
assert.deepEqual(calls, [ assert.deepEqual(calls, [

View File

@@ -54,7 +54,7 @@ test('playback handler drives mpv commands and playback state', async () => {
const reportPayloads: Array<Record<string, unknown>> = []; const reportPayloads: Array<Record<string, unknown>> = [];
const handler = createPlayJellyfinItemInMpvHandler({ const handler = createPlayJellyfinItemInMpvHandler({
ensureMpvConnectedForPlayback: async () => true, ensureMpvConnectedForPlayback: async () => true,
getMpvClient: () => ({ connected: true }), getMpvClient: () => ({ connected: true, send: () => {} }),
resolvePlaybackPlan: async () => ({ resolvePlaybackPlan: async () => ({
url: 'https://stream.example/video.m3u8', url: 'https://stream.example/video.m3u8',
mode: 'direct', mode: 'direct',

View File

@@ -1,9 +1,6 @@
type JellyfinSession = { import type { JellyfinAuthSession, JellyfinPlaybackPlan } from '../../core/services/jellyfin';
serverUrl: string; import type { JellyfinConfig } from '../../types';
accessToken: string; import type { MpvRuntimeClientLike } from '../../core/services/mpv';
userId: string;
username: string;
};
type JellyfinClientInfo = { type JellyfinClientInfo = {
clientName: string; clientName: string;
@@ -11,15 +8,6 @@ type JellyfinClientInfo = {
deviceId: string; deviceId: string;
}; };
type JellyfinPlaybackPlan = {
url: string;
mode: 'direct' | 'transcode';
title: string;
startTimeTicks: number;
audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null;
};
type ActivePlaybackState = { type ActivePlaybackState = {
itemId: string; itemId: string;
mediaSourceId: undefined; mediaSourceId: undefined;
@@ -28,26 +16,24 @@ type ActivePlaybackState = {
playMethod: 'DirectPlay' | 'Transcode'; playMethod: 'DirectPlay' | 'Transcode';
}; };
type MpvClientLike = unknown;
export function createPlayJellyfinItemInMpvHandler(deps: { export function createPlayJellyfinItemInMpvHandler(deps: {
ensureMpvConnectedForPlayback: () => Promise<boolean>; ensureMpvConnectedForPlayback: () => Promise<boolean>;
getMpvClient: () => MpvClientLike | null; getMpvClient: () => MpvRuntimeClientLike | null;
resolvePlaybackPlan: (params: { resolvePlaybackPlan: (params: {
session: JellyfinSession; session: JellyfinAuthSession;
clientInfo: JellyfinClientInfo; clientInfo: JellyfinClientInfo;
jellyfinConfig: unknown; jellyfinConfig: JellyfinConfig;
itemId: string; itemId: string;
audioStreamIndex?: number | null; audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null; subtitleStreamIndex?: number | null;
}) => Promise<JellyfinPlaybackPlan>; }) => Promise<JellyfinPlaybackPlan>;
applyJellyfinMpvDefaults: (mpvClient: MpvClientLike) => void; applyJellyfinMpvDefaults: (mpvClient: MpvRuntimeClientLike) => void;
sendMpvCommand: (command: Array<string | number>) => void; sendMpvCommand: (command: Array<string | number>) => void;
armQuitOnDisconnect: () => void; armQuitOnDisconnect: () => void;
schedule: (callback: () => void, delayMs: number) => void; schedule: (callback: () => void, delayMs: number) => void;
convertTicksToSeconds: (ticks: number) => number; convertTicksToSeconds: (ticks: number) => number;
preloadExternalSubtitles: (params: { preloadExternalSubtitles: (params: {
session: JellyfinSession; session: JellyfinAuthSession;
clientInfo: JellyfinClientInfo; clientInfo: JellyfinClientInfo;
itemId: string; itemId: string;
}) => void; }) => void;
@@ -64,9 +50,9 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
showMpvOsd: (text: string) => void; showMpvOsd: (text: string) => void;
}) { }) {
return async (params: { return async (params: {
session: JellyfinSession; session: JellyfinAuthSession;
clientInfo: JellyfinClientInfo; clientInfo: JellyfinClientInfo;
jellyfinConfig: unknown; jellyfinConfig: JellyfinConfig;
itemId: string; itemId: string;
audioStreamIndex?: number | null; audioStreamIndex?: number | null;
subtitleStreamIndex?: number | null; subtitleStreamIndex?: number | null;
@@ -96,7 +82,11 @@ export function createPlayJellyfinItemInMpvHandler(deps: {
if (params.setQuitOnDisconnectArm !== false) { if (params.setQuitOnDisconnectArm !== false) {
deps.armQuitOnDisconnect(); deps.armQuitOnDisconnect();
} }
deps.sendMpvCommand(['set_property', 'force-media-title', `[Jellyfin/${plan.mode}] ${plan.title}`]); deps.sendMpvCommand([
'set_property',
'force-media-title',
`[Jellyfin/${plan.mode}] ${plan.title}`,
]);
deps.sendMpvCommand(['set_property', 'sid', 'no']); deps.sendMpvCommand(['set_property', 'sid', 'no']);
deps.schedule(() => { deps.schedule(() => {
deps.sendMpvCommand(['set_property', 'sid', 'no']); deps.sendMpvCommand(['set_property', 'sid', 'no']);

View File

@@ -1,5 +1,7 @@
import type { Config } from '../../types';
export type MpvClientRuntimeServiceOptions = { export type MpvClientRuntimeServiceOptions = {
getResolvedConfig: () => unknown; getResolvedConfig: () => Config;
autoStartOverlay: boolean; autoStartOverlay: boolean;
setOverlayVisible: (visible: boolean) => void; setOverlayVisible: (visible: boolean) => void;
shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; shouldBindVisibleOverlayToMpvSubVisibility: () => boolean;

View File

@@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults main deps builder maps callbacks', () => {
jellyfinLangPref: 'ja,jp', jellyfinLangPref: 'ja,jp',
})(); })();
deps.sendMpvCommandRuntime({}, ['set_property', 'aid', 'auto']); deps.sendMpvCommandRuntime({ connected: true, send: () => {} }, ['set_property', 'aid', 'auto']);
assert.equal(deps.jellyfinLangPref, 'ja,jp'); assert.equal(deps.jellyfinLangPref, 'ja,jp');
assert.deepEqual(calls, ['set_property:aid:auto']); assert.deepEqual(calls, ['set_property:aid:auto']);
}); });

View File

@@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults sends expected property commands', () => {
jellyfinLangPref: 'ja,jp', jellyfinLangPref: 'ja,jp',
}); });
applyDefaults({}); applyDefaults({ connected: true, send: () => {} });
assert.deepEqual(calls, [ assert.deepEqual(calls, [
'set_property:sub-auto:fuzzy', 'set_property:sub-auto:fuzzy',
'set_property:aid:auto', 'set_property:aid:auto',

View File

@@ -1,13 +1,10 @@
type MpvClientLike = unknown; import type { MpvRuntimeClientLike } from '../../core/services/mpv';
export function createApplyJellyfinMpvDefaultsHandler(deps: { export function createApplyJellyfinMpvDefaultsHandler(deps: {
sendMpvCommandRuntime: ( sendMpvCommandRuntime: (client: MpvRuntimeClientLike, command: [string, string, string]) => void;
client: MpvClientLike,
command: [string, string, string],
) => void;
jellyfinLangPref: string; jellyfinLangPref: string;
}) { }) {
return (client: MpvClientLike): void => { return (client: MpvRuntimeClientLike): void => {
deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']); deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']);
deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']); deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']);
deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']); deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']);
@@ -18,9 +15,7 @@ export function createApplyJellyfinMpvDefaultsHandler(deps: {
}; };
} }
export function createGetDefaultSocketPathHandler(deps: { export function createGetDefaultSocketPathHandler(deps: { platform: string }) {
platform: string;
}) {
return (): string => { return (): string => {
if (deps.platform === 'win32') { if (deps.platform === 'win32') {
return '\\\\.\\pipe\\subminer-socket'; return '\\\\.\\pipe\\subminer-socket';

View File

@@ -15,9 +15,7 @@ import {
createHandleMpvTimePosChangeHandler, createHandleMpvTimePosChangeHandler,
} from './mpv-main-event-actions'; } from './mpv-main-event-actions';
type MpvEventClient = { type MpvEventClient = Parameters<ReturnType<typeof createBindMpvClientEventHandlers>>[0];
on: (...args: any[]) => unknown;
};
export function createBindMpvMainEventHandlersHandler(deps: { export function createBindMpvMainEventHandlersHandler(deps: {
reportJellyfinRemoteStopped: () => void; reportJellyfinRemoteStopped: () => void;
@@ -119,7 +117,8 @@ export function createBindMpvMainEventHandlersHandler(deps: {
updateSubtitleRenderMetrics: (patch) => deps.updateSubtitleRenderMetrics(patch), updateSubtitleRenderMetrics: (patch) => deps.updateSubtitleRenderMetrics(patch),
}); });
const handleMpvSecondarySubtitleVisibility = createHandleMpvSecondarySubtitleVisibilityHandler({ const handleMpvSecondarySubtitleVisibility = createHandleMpvSecondarySubtitleVisibilityHandler({
setPreviousSecondarySubVisibility: (visible) => deps.setPreviousSecondarySubVisibility(visible), setPreviousSecondarySubVisibility: (visible) =>
deps.setPreviousSecondarySubVisibility(visible),
}); });
createBindMpvClientEventHandlers({ createBindMpvClientEventHandlers({
@@ -134,6 +133,6 @@ export function createBindMpvMainEventHandlersHandler(deps: {
onPauseChange: handleMpvPauseChange, onPauseChange: handleMpvPauseChange,
onSubtitleMetricsChange: handleMpvSubtitleMetricsChange, onSubtitleMetricsChange: handleMpvSubtitleMetricsChange,
onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility, onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility,
})(mpvClient as never); })(mpvClient);
}; };
} }

View File

@@ -32,7 +32,10 @@ test('append to mpv log main deps map filesystem functions and log path', async
test('show mpv osd main deps map runtime delegates and logging callback', () => { test('show mpv osd main deps map runtime delegates and logging callback', () => {
const calls: string[] = []; const calls: string[] = [];
const client = { id: 'mpv' }; const client = {
connected: true,
send: () => {},
};
const deps = createBuildShowMpvOsdMainDepsHandler({ const deps = createBuildShowMpvOsdMainDepsHandler({
appendToMpvLog: (message) => calls.push(`append:${message}`), appendToMpvLog: (message) => calls.push(`append:${message}`),
showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => { showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => {

View File

@@ -1,11 +1,10 @@
export function createBuildAppendToMpvLogMainDepsHandler(deps: { import type { createAppendToMpvLogHandler, createShowMpvOsdHandler } from './mpv-osd-log';
logPath: string;
dirname: (targetPath: string) => string; type AppendToMpvLogMainDeps = Parameters<typeof createAppendToMpvLogHandler>[0];
mkdir: (targetPath: string, options: { recursive: boolean }) => Promise<void>; type ShowMpvOsdMainDeps = Parameters<typeof createShowMpvOsdHandler>[0];
appendFile: (targetPath: string, data: string, options: { encoding: 'utf8' }) => Promise<void>;
now: () => Date; export function createBuildAppendToMpvLogMainDepsHandler(deps: AppendToMpvLogMainDeps) {
}) { return (): AppendToMpvLogMainDeps => ({
return () => ({
logPath: deps.logPath, logPath: deps.logPath,
dirname: (targetPath: string) => deps.dirname(targetPath), dirname: (targetPath: string) => deps.dirname(targetPath),
mkdir: (targetPath: string, options: { recursive: boolean }) => deps.mkdir(targetPath, options), mkdir: (targetPath: string, options: { recursive: boolean }) => deps.mkdir(targetPath, options),
@@ -15,24 +14,12 @@ export function createBuildAppendToMpvLogMainDepsHandler(deps: {
}); });
} }
export function createBuildShowMpvOsdMainDepsHandler(deps: { export function createBuildShowMpvOsdMainDepsHandler(deps: ShowMpvOsdMainDeps) {
appendToMpvLog: (message: string) => void; return (): ShowMpvOsdMainDeps => ({
showMpvOsdRuntime: (
mpvClient: unknown | null,
text: string,
fallbackLog: (line: string) => void,
) => void;
getMpvClient: () => unknown | null;
logInfo: (line: string) => void;
}) {
return () => ({
appendToMpvLog: (message: string) => deps.appendToMpvLog(message), appendToMpvLog: (message: string) => deps.appendToMpvLog(message),
showMpvOsdRuntime: ( showMpvOsdRuntime: (mpvClient, text, fallbackLog) =>
mpvClient: unknown | null, deps.showMpvOsdRuntime(mpvClient, text, fallbackLog),
text: string, getMpvClient: () => deps.getMpvClient(),
fallbackLog: (line: string) => void,
) => deps.showMpvOsdRuntime(mpvClient, text, fallbackLog),
getMpvClient: () => deps.getMpvClient() as never,
logInfo: (line: string) => deps.logInfo(line), logInfo: (line: string) => deps.logInfo(line),
}); });
} }

View File

@@ -1,15 +1,17 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { BaseWindowTracker } from '../../window-trackers';
import type { KikuFieldGroupingChoice } from '../../types';
import { createOverlayRuntimeBootstrapHandlers } from './overlay-runtime-bootstrap-handlers'; import { createOverlayRuntimeBootstrapHandlers } from './overlay-runtime-bootstrap-handlers';
test('overlay runtime bootstrap handlers compose options builder and bootstrap handler', () => { test('overlay runtime bootstrap handlers compose options builder and bootstrap handler', () => {
const appState = { const appState = {
backendOverride: null as string | null, backendOverride: null as string | null,
windowTracker: null as unknown, windowTracker: null as BaseWindowTracker | null,
subtitleTimingTracker: null as unknown, subtitleTimingTracker: null as unknown,
mpvClient: null as unknown, mpvClient: null,
mpvSocketPath: '/tmp/mpv.sock', mpvSocketPath: '/tmp/mpv.sock',
runtimeOptionsManager: null as unknown, runtimeOptionsManager: null,
ankiIntegration: null as unknown, ankiIntegration: null as unknown,
}; };
let initialized = false; let initialized = false;
@@ -39,7 +41,13 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h
getOverlayWindows: () => [], getOverlayWindows: () => [],
getResolvedConfig: () => ({}), getResolvedConfig: () => ({}),
showDesktopNotification: () => {}, showDesktopNotification: () => {},
createFieldGroupingCallback: () => (async () => 'combined' as never), createFieldGroupingCallback: () => async () =>
({
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: false,
cancelled: true,
}) as KikuFieldGroupingChoice,
getKnownWordCacheStatePath: () => '/tmp/known.json', getKnownWordCacheStatePath: () => '/tmp/known.json',
}, },
initializeOverlayRuntimeBootstrapDeps: { initializeOverlayRuntimeBootstrapDeps: {

View File

@@ -6,17 +6,24 @@ import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-ru
type InitializeOverlayRuntimeMainDeps = Parameters< type InitializeOverlayRuntimeMainDeps = Parameters<
typeof createBuildInitializeOverlayRuntimeMainDepsHandler typeof createBuildInitializeOverlayRuntimeMainDepsHandler
>[0]; >[0];
type InitializeOverlayRuntimeOptions = ReturnType<
ReturnType<typeof createBuildInitializeOverlayRuntimeOptionsHandler>
>;
type InitializeOverlayRuntimeBootstrapMainDeps = Parameters< type InitializeOverlayRuntimeBootstrapMainDeps = Parameters<
typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler<InitializeOverlayRuntimeOptions>
>[0]; >[0];
export function createOverlayRuntimeBootstrapHandlers(deps: { export function createOverlayRuntimeBootstrapHandlers(deps: {
initializeOverlayRuntimeMainDeps: InitializeOverlayRuntimeMainDeps; initializeOverlayRuntimeMainDeps: InitializeOverlayRuntimeMainDeps;
initializeOverlayRuntimeBootstrapDeps: Omit<InitializeOverlayRuntimeBootstrapMainDeps, 'buildOptions'>; initializeOverlayRuntimeBootstrapDeps: Omit<
InitializeOverlayRuntimeBootstrapMainDeps,
'buildOptions'
>;
}) { }) {
const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler( const buildInitializeOverlayRuntimeOptionsHandler =
createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(), createBuildInitializeOverlayRuntimeOptionsHandler(
); createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(),
);
const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler( const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler(
createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler({ createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler({
...deps.initializeOverlayRuntimeBootstrapDeps, ...deps.initializeOverlayRuntimeBootstrapDeps,

View File

@@ -1,12 +1,13 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import type { BaseWindowTracker } from '../../window-trackers';
import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-runtime-options-main-deps'; import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-runtime-options-main-deps';
test('overlay runtime main deps builder maps runtime state and callbacks', () => { test('overlay runtime main deps builder maps runtime state and callbacks', () => {
const calls: string[] = []; const calls: string[] = [];
const appState = { const appState = {
backendOverride: 'x11' as string | null, backendOverride: 'x11' as string | null,
windowTracker: null as unknown, windowTracker: null as BaseWindowTracker | null,
subtitleTimingTracker: { id: 'tracker' } as unknown, subtitleTimingTracker: { id: 'tracker' } as unknown,
mpvClient: null as { send?: (payload: { command: string[] }) => void } | null, mpvClient: null as { send?: (payload: { command: string[] }) => void } | null,
mpvSocketPath: '/tmp/mpv.sock', mpvSocketPath: '/tmp/mpv.sock',
@@ -36,7 +37,12 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
getOverlayWindows: () => [], getOverlayWindows: () => [],
getResolvedConfig: () => ({}), getResolvedConfig: () => ({}),
showDesktopNotification: () => calls.push('notify'), showDesktopNotification: () => calls.push('notify'),
createFieldGroupingCallback: () => async () => ({ cancelled: true }), createFieldGroupingCallback: () => async () => ({
keepNoteId: 1,
deleteNoteId: 2,
deleteDuplicate: false,
cancelled: true,
}),
getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json', getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json',
}); });
@@ -58,7 +64,11 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
deps.syncOverlayShortcuts(); deps.syncOverlayShortcuts();
deps.showDesktopNotification('title', {}); deps.showDesktopNotification('title', {});
deps.setWindowTracker({ id: 'tracker' }); const tracker = {
close: () => {},
getWindowGeometry: () => null,
} as unknown as BaseWindowTracker;
deps.setWindowTracker(tracker);
deps.setAnkiIntegration({ id: 'anki' }); deps.setAnkiIntegration({ id: 'anki' });
assert.deepEqual(calls, [ assert.deepEqual(calls, [
@@ -72,6 +82,6 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () =>
'sync-shortcuts', 'sync-shortcuts',
'notify', 'notify',
]); ]);
assert.deepEqual(appState.windowTracker, { id: 'tracker' }); assert.equal(appState.windowTracker, tracker);
assert.deepEqual(appState.ankiIntegration, { id: 'anki' }); assert.deepEqual(appState.ankiIntegration, { id: 'anki' });
}); });

View File

@@ -1,14 +1,19 @@
import type { AnkiConnectConfig } from '../../types'; import type { AnkiConnectConfig } from '../../types';
import type { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options';
type OverlayRuntimeOptionsMainDeps = Parameters<
typeof createBuildInitializeOverlayRuntimeOptionsHandler
>[0];
export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
appState: { appState: {
backendOverride: string | null; backendOverride: string | null;
windowTracker: unknown | null; windowTracker: Parameters<OverlayRuntimeOptionsMainDeps['setWindowTracker']>[0];
subtitleTimingTracker: unknown | null; subtitleTimingTracker: ReturnType<OverlayRuntimeOptionsMainDeps['getSubtitleTimingTracker']>;
mpvClient: unknown | null; mpvClient: ReturnType<OverlayRuntimeOptionsMainDeps['getMpvClient']>;
mpvSocketPath: string; mpvSocketPath: string;
runtimeOptionsManager: unknown | null; runtimeOptionsManager: ReturnType<OverlayRuntimeOptionsMainDeps['getRuntimeOptionsManager']>;
ankiIntegration: unknown | null; ankiIntegration: Parameters<OverlayRuntimeOptionsMainDeps['setAnkiIntegration']>[0];
}; };
overlayManager: { overlayManager: {
getVisibleOverlayVisible: () => boolean; getVisibleOverlayVisible: () => boolean;
@@ -25,27 +30,36 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
createMainWindow: () => void; createMainWindow: () => void;
createInvisibleWindow: () => void; createInvisibleWindow: () => void;
registerGlobalShortcuts: () => void; registerGlobalShortcuts: () => void;
updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) => void; updateVisibleOverlayBounds: (geometry: {
x: number;
y: number;
width: number;
height: number;
}) => void;
updateInvisibleOverlayBounds: (geometry: { updateInvisibleOverlayBounds: (geometry: {
x: number; x: number;
y: number; y: number;
width: number; width: number;
height: number; height: number;
}) => void; }) => void;
getOverlayWindows: () => unknown[]; getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows'];
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void; showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void;
createFieldGroupingCallback: () => unknown; createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback'];
getKnownWordCacheStatePath: () => string; getKnownWordCacheStatePath: () => string;
}) { }) {
return () => ({ return (): OverlayRuntimeOptionsMainDeps => ({
getBackendOverride: () => deps.appState.backendOverride, getBackendOverride: () => deps.appState.backendOverride,
getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(), getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(),
createMainWindow: () => deps.createMainWindow(), createMainWindow: () => deps.createMainWindow(),
createInvisibleWindow: () => deps.createInvisibleWindow(), createInvisibleWindow: () => deps.createInvisibleWindow(),
registerGlobalShortcuts: () => deps.registerGlobalShortcuts(), registerGlobalShortcuts: () => deps.registerGlobalShortcuts(),
updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) => updateVisibleOverlayBounds: (geometry: {
deps.updateVisibleOverlayBounds(geometry), x: number;
y: number;
width: number;
height: number;
}) => deps.updateVisibleOverlayBounds(geometry),
updateInvisibleOverlayBounds: (geometry: { updateInvisibleOverlayBounds: (geometry: {
x: number; x: number;
y: number; y: number;
@@ -54,28 +68,25 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: {
}) => deps.updateInvisibleOverlayBounds(geometry), }) => deps.updateInvisibleOverlayBounds(geometry),
isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(), isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(),
isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(), isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(),
updateVisibleOverlayVisibility: () => deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), updateVisibleOverlayVisibility: () =>
deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(),
updateInvisibleOverlayVisibility: () => updateInvisibleOverlayVisibility: () =>
deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(), deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(),
getOverlayWindows: () => deps.getOverlayWindows() as never, getOverlayWindows: () => deps.getOverlayWindows(),
syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(), syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(),
setWindowTracker: (tracker: unknown | null) => { setWindowTracker: (tracker) => {
deps.appState.windowTracker = tracker; deps.appState.windowTracker = tracker;
}, },
getResolvedConfig: () => deps.getResolvedConfig(), getResolvedConfig: () => deps.getResolvedConfig(),
getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker, getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker,
getMpvClient: () => getMpvClient: () => deps.appState.mpvClient,
(deps.appState.mpvClient as { send?: (payload: { command: string[] }) => void } | null),
getMpvSocketPath: () => deps.appState.mpvSocketPath, getMpvSocketPath: () => deps.appState.mpvSocketPath,
getRuntimeOptionsManager: () => getRuntimeOptionsManager: () => deps.appState.runtimeOptionsManager,
deps.appState.runtimeOptionsManager as setAnkiIntegration: (integration) => {
| { getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig }
| null,
setAnkiIntegration: (integration: unknown | null) => {
deps.appState.ankiIntegration = integration; deps.appState.ankiIntegration = integration;
}, },
showDesktopNotification: deps.showDesktopNotification, showDesktopNotification: deps.showDesktopNotification,
createFieldGroupingCallback: () => deps.createFieldGroupingCallback() as never, createFieldGroupingCallback: () => deps.createFieldGroupingCallback(),
getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(), getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(),
}); });
} }

View File

@@ -5,6 +5,7 @@ import type {
WindowGeometry, WindowGeometry,
} from '../../types'; } from '../../types';
import type { BrowserWindow } from 'electron'; import type { BrowserWindow } from 'electron';
import type { BaseWindowTracker } from '../../window-trackers';
type OverlayRuntimeOptions = { type OverlayRuntimeOptions = {
backendOverride: string | null; backendOverride: string | null;
@@ -20,7 +21,7 @@ type OverlayRuntimeOptions = {
updateInvisibleOverlayVisibility: () => void; updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[]; getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: unknown | null) => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void;
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
getSubtitleTimingTracker: () => unknown | null; getSubtitleTimingTracker: () => unknown | null;
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null; getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;
@@ -50,7 +51,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: {
updateInvisibleOverlayVisibility: () => void; updateInvisibleOverlayVisibility: () => void;
getOverlayWindows: () => BrowserWindow[]; getOverlayWindows: () => BrowserWindow[];
syncOverlayShortcuts: () => void; syncOverlayShortcuts: () => void;
setWindowTracker: (tracker: unknown | null) => void; setWindowTracker: (tracker: BaseWindowTracker | null) => void;
getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig };
getSubtitleTimingTracker: () => unknown | null; getSubtitleTimingTracker: () => unknown | null;
getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null; getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null;

View File

@@ -1,3 +1,5 @@
import type { BaseWindowTracker } from '../../window-trackers';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './overlay-visibility-runtime-main-deps'; import { createBuildOverlayVisibilityRuntimeMainDepsHandler } from './overlay-visibility-runtime-main-deps';
@@ -7,13 +9,14 @@ test('overlay visibility runtime main deps builder maps state and geometry callb
let trackerNotReadyWarningShown = false; let trackerNotReadyWarningShown = false;
const mainWindow = { id: 'main' } as never; const mainWindow = { id: 'main' } as never;
const invisibleWindow = { id: 'invisible' } as never; const invisibleWindow = { id: 'invisible' } as never;
const tracker = { id: 'tracker' } as unknown as BaseWindowTracker;
const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({ const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({
getMainWindow: () => mainWindow, getMainWindow: () => mainWindow,
getInvisibleWindow: () => invisibleWindow, getInvisibleWindow: () => invisibleWindow,
getVisibleOverlayVisible: () => true, getVisibleOverlayVisible: () => true,
getInvisibleOverlayVisible: () => false, getInvisibleOverlayVisible: () => false,
getWindowTracker: () => ({ id: 'tracker' }), getWindowTracker: () => tracker,
getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown, getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown,
setTrackerNotReadyWarningShown: (shown) => { setTrackerNotReadyWarningShown: (shown) => {
trackerNotReadyWarningShown = shown; trackerNotReadyWarningShown = shown;

View File

@@ -2,29 +2,19 @@ import type { BrowserWindow } from 'electron';
import type { WindowGeometry } from '../../types'; import type { WindowGeometry } from '../../types';
import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime'; import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime';
export function createBuildOverlayVisibilityRuntimeMainDepsHandler(deps: { export function createBuildOverlayVisibilityRuntimeMainDepsHandler(
getMainWindow: () => BrowserWindow | null; deps: OverlayVisibilityRuntimeDeps,
getInvisibleWindow: () => BrowserWindow | null; ) {
getVisibleOverlayVisible: () => boolean;
getInvisibleOverlayVisible: () => boolean;
getWindowTracker: () => unknown | null;
getTrackerNotReadyWarningShown: () => boolean;
setTrackerNotReadyWarningShown: (shown: boolean) => void;
updateVisibleOverlayBounds: (geometry: WindowGeometry) => void;
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => void;
ensureOverlayWindowLevel: (window: BrowserWindow) => void;
enforceOverlayLayerOrder: () => void;
syncOverlayShortcuts: () => void;
}) {
return (): OverlayVisibilityRuntimeDeps => ({ return (): OverlayVisibilityRuntimeDeps => ({
getMainWindow: () => deps.getMainWindow(), getMainWindow: () => deps.getMainWindow(),
getInvisibleWindow: () => deps.getInvisibleWindow(), getInvisibleWindow: () => deps.getInvisibleWindow(),
getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(),
getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(),
getWindowTracker: () => deps.getWindowTracker() as never, getWindowTracker: () => deps.getWindowTracker(),
getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(), getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(),
setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown), setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown),
updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry), updateVisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateVisibleOverlayBounds(geometry),
updateInvisibleOverlayBounds: (geometry: WindowGeometry) => updateInvisibleOverlayBounds: (geometry: WindowGeometry) =>
deps.updateInvisibleOverlayBounds(geometry), deps.updateInvisibleOverlayBounds(geometry),
ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window),

View File

@@ -8,7 +8,7 @@ import {
test('reload config main deps builder maps callbacks and fail handlers', async () => { test('reload config main deps builder maps callbacks and fail handlers', async () => {
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildReloadConfigMainDepsHandler({ const deps = createBuildReloadConfigMainDepsHandler({
reloadConfigStrict: () => ({ ok: true }), reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }),
logInfo: (message) => calls.push(`info:${message}`), logInfo: (message) => calls.push(`info:${message}`),
logWarning: (message) => calls.push(`warn:${message}`), logWarning: (message) => calls.push(`warn:${message}`),
showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`), showDesktopNotification: (title, options) => calls.push(`notify:${title}:${options.body}`),
@@ -24,7 +24,11 @@ test('reload config main deps builder maps callbacks and fail handlers', async (
}, },
})(); })();
assert.deepEqual(deps.reloadConfigStrict(), { ok: true }); assert.deepEqual(deps.reloadConfigStrict(), {
ok: true,
path: '/tmp/config.jsonc',
warnings: [],
});
deps.logInfo('x'); deps.logInfo('x');
deps.logWarning('y'); deps.logWarning('y');
deps.showDesktopNotification('SubMiner', { body: 'warn' }); deps.showDesktopNotification('SubMiner', { body: 'warn' });

View File

@@ -1,18 +1,11 @@
export function createBuildReloadConfigMainDepsHandler(deps: { import type { createCriticalConfigErrorHandler, createReloadConfigHandler } from './startup-config';
reloadConfigStrict: () => unknown;
logInfo: (message: string) => void; type ReloadConfigMainDeps = Parameters<typeof createReloadConfigHandler>[0];
logWarning: (message: string) => void; type CriticalConfigErrorMainDeps = Parameters<typeof createCriticalConfigErrorHandler>[0];
showDesktopNotification: (title: string, options: { body: string }) => void;
startConfigHotReload: () => void; export function createBuildReloadConfigMainDepsHandler(deps: ReloadConfigMainDeps) {
refreshAnilistClientSecretState: (options: { force: boolean }) => Promise<unknown>; return (): ReloadConfigMainDeps => ({
failHandlers: { reloadConfigStrict: () => deps.reloadConfigStrict(),
logError: (details: string) => void;
showErrorBox: (title: string, details: string) => void;
quit: () => void;
};
}) {
return () => ({
reloadConfigStrict: () => deps.reloadConfigStrict() as never,
logInfo: (message: string) => deps.logInfo(message), logInfo: (message: string) => deps.logInfo(message),
logWarning: (message: string) => deps.logWarning(message), logWarning: (message: string) => deps.logWarning(message),
showDesktopNotification: (title: string, options: { body: string }) => showDesktopNotification: (title: string, options: { body: string }) =>
@@ -22,25 +15,20 @@ export function createBuildReloadConfigMainDepsHandler(deps: {
deps.refreshAnilistClientSecretState(options), deps.refreshAnilistClientSecretState(options),
failHandlers: { failHandlers: {
logError: (details: string) => deps.failHandlers.logError(details), logError: (details: string) => deps.failHandlers.logError(details),
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details), showErrorBox: (title: string, details: string) =>
deps.failHandlers.showErrorBox(title, details),
quit: () => deps.failHandlers.quit(), quit: () => deps.failHandlers.quit(),
}, },
}); });
} }
export function createBuildCriticalConfigErrorMainDepsHandler(deps: { export function createBuildCriticalConfigErrorMainDepsHandler(deps: CriticalConfigErrorMainDeps) {
getConfigPath: () => string; return (): CriticalConfigErrorMainDeps => ({
failHandlers: {
logError: (details: string) => void;
showErrorBox: (title: string, details: string) => void;
quit: () => void;
};
}) {
return () => ({
getConfigPath: () => deps.getConfigPath(), getConfigPath: () => deps.getConfigPath(),
failHandlers: { failHandlers: {
logError: (details: string) => deps.failHandlers.logError(details), logError: (details: string) => deps.failHandlers.logError(details),
showErrorBox: (title: string, details: string) => deps.failHandlers.showErrorBox(title, details), showErrorBox: (title: string, details: string) =>
deps.failHandlers.showErrorBox(title, details),
quit: () => deps.failHandlers.quit(), quit: () => deps.failHandlers.quit(),
}, },
}); });

View File

@@ -9,8 +9,8 @@ import {
test('tokenizer deps builder records known-word lookups and maps readers', () => { test('tokenizer deps builder records known-word lookups and maps readers', () => {
const calls: string[] = []; const calls: string[] = [];
const deps = createBuildTokenizerDepsMainHandler({ const deps = createBuildTokenizerDepsMainHandler({
getYomitanExt: () => ({ id: 'ext' }), getYomitanExt: () => null,
getYomitanParserWindow: () => ({ id: 'window' }), getYomitanParserWindow: () => null,
setYomitanParserWindow: () => calls.push('set-window'), setYomitanParserWindow: () => calls.push('set-window'),
getYomitanParserReadyPromise: () => null, getYomitanParserReadyPromise: () => null,
setYomitanParserReadyPromise: () => calls.push('set-ready'), setYomitanParserReadyPromise: () => calls.push('set-ready'),
@@ -18,22 +18,22 @@ test('tokenizer deps builder records known-word lookups and maps readers', () =>
setYomitanParserInitPromise: () => calls.push('set-init'), setYomitanParserInitPromise: () => calls.push('set-init'),
isKnownWord: (text) => text === 'known', isKnownWord: (text) => text === 'known',
recordLookup: (hit) => calls.push(`lookup:${hit}`), recordLookup: (hit) => calls.push(`lookup:${hit}`),
getKnownWordMatchMode: () => 'exact', getKnownWordMatchMode: () => 'surface',
getMinSentenceWordsForNPlusOne: () => 3, getMinSentenceWordsForNPlusOne: () => 3,
getJlptLevel: () => 'N2', getJlptLevel: () => 'N2',
getJlptEnabled: () => true, getJlptEnabled: () => true,
getFrequencyDictionaryEnabled: () => true, getFrequencyDictionaryEnabled: () => true,
getFrequencyRank: () => 5, getFrequencyRank: () => 5,
getYomitanGroupDebugEnabled: () => false, getYomitanGroupDebugEnabled: () => false,
getMecabTokenizer: () => ({ id: 'mecab' }), getMecabTokenizer: () => null,
})(); })();
assert.equal(deps.isKnownWord('known'), true); assert.equal(deps.isKnownWord('known'), true);
assert.equal(deps.isKnownWord('unknown'), false); assert.equal(deps.isKnownWord('unknown'), false);
deps.setYomitanParserWindow({}); deps.setYomitanParserWindow(null);
deps.setYomitanParserReadyPromise(null); deps.setYomitanParserReadyPromise(null);
deps.setYomitanParserInitPromise(null); deps.setYomitanParserInitPromise(null);
assert.equal(deps.getMinSentenceWordsForNPlusOne(), 3); assert.equal(deps.getMinSentenceWordsForNPlusOne?.(), 3);
assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']); assert.deepEqual(calls, ['lookup:true', 'lookup:false', 'set-window', 'set-ready', 'set-init']);
}); });

View File

@@ -1,30 +1,29 @@
export function createBuildTokenizerDepsMainHandler(deps: { import type { TokenizerDepsRuntimeOptions } from '../../core/services/tokenizer';
getYomitanExt: () => unknown;
getYomitanParserWindow: () => unknown; type TokenizerMainDeps = TokenizerDepsRuntimeOptions & {
setYomitanParserWindow: (window: unknown) => void; getJlptEnabled: NonNullable<TokenizerDepsRuntimeOptions['getJlptEnabled']>;
getYomitanParserReadyPromise: () => Promise<void> | null; getFrequencyDictionaryEnabled: NonNullable<
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void; TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled']
getYomitanParserInitPromise: () => Promise<boolean> | null; >;
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void; getFrequencyRank: NonNullable<TokenizerDepsRuntimeOptions['getFrequencyRank']>;
isKnownWord: (text: string) => boolean; getMinSentenceWordsForNPlusOne: NonNullable<
TokenizerDepsRuntimeOptions['getMinSentenceWordsForNPlusOne']
>;
getYomitanGroupDebugEnabled: NonNullable<
TokenizerDepsRuntimeOptions['getYomitanGroupDebugEnabled']
>;
recordLookup: (hit: boolean) => void; recordLookup: (hit: boolean) => void;
getKnownWordMatchMode: () => unknown; };
getMinSentenceWordsForNPlusOne: () => number;
getJlptLevel: (text: string) => unknown; export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) {
getJlptEnabled: () => boolean; return (): TokenizerDepsRuntimeOptions => ({
getFrequencyDictionaryEnabled: () => boolean; getYomitanExt: () => deps.getYomitanExt(),
getFrequencyRank: (text: string) => unknown; getYomitanParserWindow: () => deps.getYomitanParserWindow(),
getYomitanGroupDebugEnabled: () => boolean; setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window),
getMecabTokenizer: () => unknown; getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(),
}) {
return () => ({
getYomitanExt: () => deps.getYomitanExt() as never,
getYomitanParserWindow: () => deps.getYomitanParserWindow() as never,
setYomitanParserWindow: (window: unknown) => deps.setYomitanParserWindow(window),
getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise() as never,
setYomitanParserReadyPromise: (promise: Promise<void> | null) => setYomitanParserReadyPromise: (promise: Promise<void> | null) =>
deps.setYomitanParserReadyPromise(promise), deps.setYomitanParserReadyPromise(promise),
getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise() as never, getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise(),
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => setYomitanParserInitPromise: (promise: Promise<boolean> | null) =>
deps.setYomitanParserInitPromise(promise), deps.setYomitanParserInitPromise(promise),
isKnownWord: (text: string) => { isKnownWord: (text: string) => {
@@ -32,14 +31,14 @@ export function createBuildTokenizerDepsMainHandler(deps: {
deps.recordLookup(hit); deps.recordLookup(hit);
return hit; return hit;
}, },
getKnownWordMatchMode: () => deps.getKnownWordMatchMode() as never, getKnownWordMatchMode: () => deps.getKnownWordMatchMode(),
getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(), getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(),
getJlptLevel: (text: string) => deps.getJlptLevel(text) as never, getJlptLevel: (text: string) => deps.getJlptLevel(text),
getJlptEnabled: () => deps.getJlptEnabled(), getJlptEnabled: () => deps.getJlptEnabled(),
getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(), getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(),
getFrequencyRank: (text: string) => deps.getFrequencyRank(text) as never, getFrequencyRank: (text: string) => deps.getFrequencyRank(text),
getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(), getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(),
getMecabTokenizer: () => deps.getMecabTokenizer() as never, getMecabTokenizer: () => deps.getMecabTokenizer(),
}); });
} }

View File

@@ -1,22 +1,34 @@
import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle'; import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle';
import { createBuildDestroyTrayMainDepsHandler, createBuildEnsureTrayMainDepsHandler } from './app-runtime-main-deps'; import {
import { createBuildTrayMenuTemplateHandler, createResolveTrayIconPathHandler } from './tray-main-actions'; createBuildDestroyTrayMainDepsHandler,
createBuildEnsureTrayMainDepsHandler,
} from './app-runtime-main-deps';
import {
createBuildTrayMenuTemplateHandler,
createResolveTrayIconPathHandler,
} from './tray-main-actions';
import { import {
createBuildResolveTrayIconPathMainDepsHandler, createBuildResolveTrayIconPathMainDepsHandler,
createBuildTrayMenuTemplateMainDepsHandler, createBuildTrayMenuTemplateMainDepsHandler,
} from './tray-main-deps'; } from './tray-main-deps';
type ResolveTrayIconPathMainDeps = Parameters<typeof createBuildResolveTrayIconPathMainDepsHandler>[0]; type ResolveTrayIconPathMainDeps = Parameters<
typeof createBuildResolveTrayIconPathMainDepsHandler
>[0];
type BuildTrayMenuTemplateMainDeps<TMenuItem> = Parameters< type BuildTrayMenuTemplateMainDeps<TMenuItem> = Parameters<
typeof createBuildTrayMenuTemplateMainDepsHandler<TMenuItem> typeof createBuildTrayMenuTemplateMainDepsHandler<TMenuItem>
>[0]; >[0];
type EnsureTrayMainDeps = Parameters<typeof createBuildEnsureTrayMainDepsHandler>[0]; type EnsureTrayMainDeps<TTrayMenu> = Parameters<
type DestroyTrayMainDeps = Parameters<typeof createBuildDestroyTrayMainDepsHandler>[0]; typeof createBuildEnsureTrayMainDepsHandler<TrayLike, TTrayMenu, TrayIconLike>
>[0];
type TrayLike = NonNullable<ReturnType<Parameters<typeof createEnsureTrayHandler>[0]['getTray']>>;
type TrayIconLike = Parameters<Parameters<typeof createEnsureTrayHandler>[0]['createTray']>[0];
type DestroyTrayMainDeps = Parameters<typeof createBuildDestroyTrayMainDepsHandler<TrayLike>>[0];
export function createTrayRuntimeHandlers<TMenuItem, TMenu>(deps: { export function createTrayRuntimeHandlers<TMenuItem, TMenu>(deps: {
resolveTrayIconPathDeps: ResolveTrayIconPathMainDeps; resolveTrayIconPathDeps: ResolveTrayIconPathMainDeps;
buildTrayMenuTemplateDeps: BuildTrayMenuTemplateMainDeps<TMenuItem>; buildTrayMenuTemplateDeps: BuildTrayMenuTemplateMainDeps<TMenuItem>;
ensureTrayDeps: Omit<EnsureTrayMainDeps, 'buildTrayMenu' | 'resolveTrayIconPath'>; ensureTrayDeps: Omit<EnsureTrayMainDeps<TMenu>, 'buildTrayMenu' | 'resolveTrayIconPath'>;
destroyTrayDeps: DestroyTrayMainDeps; destroyTrayDeps: DestroyTrayMainDeps;
buildMenuFromTemplate: (template: TMenuItem[]) => TMenu; buildMenuFromTemplate: (template: TMenuItem[]) => TMenu;
}) { }) {