diff --git a/backlog/tasks/task-105 - Eliminate-unsafe-non-test-runtime-casts-in-main-boundaries.md b/backlog/tasks/task-105 - Eliminate-unsafe-non-test-runtime-casts-in-main-boundaries.md index 3b17f12..2b9faee 100644 --- a/backlog/tasks/task-105 - Eliminate-unsafe-non-test-runtime-casts-in-main-boundaries.md +++ b/backlog/tasks/task-105 - Eliminate-unsafe-non-test-runtime-casts-in-main-boundaries.md @@ -1,10 +1,11 @@ --- id: TASK-105 title: Eliminate unsafe non-test runtime casts in main boundaries -status: To Do -assignee: [] +status: Done +assignee: + - opencode-task105-unsafe-casts created_date: '2026-02-22 07:13' -updated_date: '2026-02-22 07:13' +updated_date: '2026-02-22 21:56' labels: - refactor - type-safety @@ -36,15 +37,43 @@ Current scan shows repeated casts in dependency builders and runtime adapters, w ## Acceptance Criteria -- [ ] #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. -- [ ] #3 Contract regressions are caught by compile/test checks rather than runtime behavior. +- [x] #1 Non-test `as never` occurrences in `src/main.ts` and `src/main/runtime` are reduced to zero or documented narrow exceptions. +- [x] #2 Runtime dependency builders compile without unsafe production-path cast escapes. +- [x] #3 Contract regressions are caught by compile/test checks rather than runtime behavior. +## Implementation Plan + + +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. + + +## Implementation Notes + + +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. + + +## Final Summary + + +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. + + ## Definition of Done -- [ ] #1 Cast reduction report attached in task notes (before/after counts). -- [ ] #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] #1 Cast reduction report attached in task notes (before/after counts). +- [x] #2 `bun run build` and `bun run test:core:src` pass. +- [x] #3 Any remaining exceptions have explicit rationale in code comments or task notes. - diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md index 49e1f75..eb2fcb3 100644 --- a/docs/subagents/INDEX.md +++ b/docs/subagents/INDEX.md @@ -2,74 +2,82 @@ Read first. Keep concise. -| 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-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-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-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-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-20T02:56:34Z` | -| `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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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` | -| `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` | -| `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-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-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-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-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-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-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-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-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-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-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-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-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` | -| `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-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` | -| `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` | -| `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-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-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-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-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-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-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-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-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-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-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` | -| `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-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-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-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-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-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-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` | -| `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-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-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-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-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-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` | +| agent_id | alias | mission | status | file | last_update_utc | +| ------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------- | -------------------------------------------------------------------------------------- | ---------------------- | +| `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-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-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-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-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-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-20T02:56:34Z` | +| `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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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-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` | +| `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` | +| `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-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-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-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-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-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-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-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-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-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-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-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-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` | +| `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-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` | +| `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` | +| `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-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-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-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-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-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-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-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-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-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-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` | +| `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-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-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-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-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-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-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` | +| `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-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-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-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-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-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` | +| `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` | diff --git a/docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md b/docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md new file mode 100644 index 0000000..81db207 --- /dev/null +++ b/docs/subagents/agents/opencode-task105-unsafe-casts-20260222T194704Z-zfcm.md @@ -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. diff --git a/docs/subagents/collaboration.md b/docs/subagents/collaboration.md index ae58e9e..19c30ce 100644 --- a/docs/subagents/collaboration.md +++ b/docs/subagents/collaboration.md @@ -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-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: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`). @@ -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 (`...//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: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. diff --git a/src/main.ts b/src/main.ts index 658095d..94dc502 100644 --- a/src/main.ts +++ b/src/main.ts @@ -514,7 +514,7 @@ let yomitanLoadInFlight: Promise | null = null; const buildApplyJellyfinMpvDefaultsMainDepsHandler = createBuildApplyJellyfinMpvDefaultsMainDepsHandler({ - sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client as never, command), + sendMpvCommandRuntime: (client, command) => sendMpvCommandRuntime(client, command), jellyfinLangPref: JELLYFIN_LANG_PREF, }); const applyJellyfinMpvDefaultsMainDeps = buildApplyJellyfinMpvDefaultsMainDepsHandler(); @@ -522,7 +522,9 @@ const applyJellyfinMpvDefaultsHandler = createApplyJellyfinMpvDefaultsHandler( applyJellyfinMpvDefaultsMainDeps, ); -function applyJellyfinMpvDefaults(client: MpvIpcClient): void { +function applyJellyfinMpvDefaults( + client: Parameters[0], +): void { applyJellyfinMpvDefaultsHandler(client); } @@ -718,36 +720,35 @@ const subsyncRuntime = createMainSubsyncRuntime(buildMainSubsyncRuntimeMainDepsH let appTray: Tray | null = null; const buildSubtitleProcessingControllerMainDepsHandler = createBuildSubtitleProcessingControllerMainDepsHandler({ - tokenizeSubtitle: async (text: string) => { - if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) { - return null; - } - return await tokenizeSubtitle(text); - }, - emitSubtitle: (payload) => { - const previousSubtitleText = appState.currentSubtitleData?.text ?? null; - const nextSubtitleText = payload?.text ?? null; - const subtitleChanged = previousSubtitleText !== nextSubtitleText; - appState.currentSubtitleData = payload; - if (subtitleChanged) { - appState.hoveredSubtitleTokenIndex = null; - appState.hoveredSubtitleRevision += 1; - applyHoveredTokenOverlay(); - } - broadcastToOverlayWindows('subtitle:set', payload); - subtitleWsService.broadcast(payload, { - enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, - topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, - mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, - }); - }, - logDebug: (message) => { - logger.debug(`[subtitle-processing] ${message}`); - }, - now: () => Date.now(), -}); -const subtitleProcessingControllerMainDeps = - buildSubtitleProcessingControllerMainDepsHandler(); + tokenizeSubtitle: async (text: string) => { + if (getOverlayWindows().length === 0 && !subtitleWsService.hasClients()) { + return null; + } + return await tokenizeSubtitle(text); + }, + emitSubtitle: (payload) => { + const previousSubtitleText = appState.currentSubtitleData?.text ?? null; + const nextSubtitleText = payload?.text ?? null; + const subtitleChanged = previousSubtitleText !== nextSubtitleText; + appState.currentSubtitleData = payload; + if (subtitleChanged) { + appState.hoveredSubtitleTokenIndex = null; + appState.hoveredSubtitleRevision += 1; + applyHoveredTokenOverlay(); + } + broadcastToOverlayWindows('subtitle:set', payload); + subtitleWsService.broadcast(payload, { + enabled: getResolvedConfig().subtitleStyle.frequencyDictionary.enabled, + topX: getResolvedConfig().subtitleStyle.frequencyDictionary.topX, + mode: getResolvedConfig().subtitleStyle.frequencyDictionary.mode, + }); + }, + logDebug: (message) => { + logger.debug(`[subtitle-processing] ${message}`); + }, + now: () => Date.now(), + }); +const subtitleProcessingControllerMainDeps = buildSubtitleProcessingControllerMainDepsHandler(); const subtitleProcessingController = createSubtitleProcessingController( subtitleProcessingControllerMainDeps, ); @@ -811,20 +812,20 @@ const watchConfigPathHandler = createWatchConfigPathHandler(buildWatchConfigPath const buildConfigHotReloadAppliedMainDepsHandler = createBuildConfigHotReloadAppliedMainDepsHandler( { setKeybindings: (keybindings) => { - appState.keybindings = keybindings as never; + appState.keybindings = keybindings; }, refreshGlobalAndOverlayShortcuts: () => { refreshGlobalAndOverlayShortcuts(); }, setSecondarySubMode: (mode) => { - appState.secondarySubMode = mode as never; + appState.secondarySubMode = mode; }, broadcastToOverlayWindows: (channel, payload) => { broadcastToOverlayWindows(channel, payload); }, applyAnkiRuntimeConfigPatch: (patch) => { if (appState.ankiIntegration) { - appState.ankiIntegration.applyRuntimeConfigPatch(patch as never); + appState.ankiIntegration.applyRuntimeConfigPatch(patch); } }, }, @@ -912,7 +913,7 @@ const jlptDictionaryRuntime = createJlptDictionaryRuntimeService( getDictionaryRoots: () => buildDictionaryRootsHandler(), getJlptDictionarySearchPaths, setJlptLevelLookup: (lookup) => { - appState.jlptLevelLookup = lookup as never; + appState.jlptLevelLookup = lookup; }, logInfo: (message) => logger.info(message), })(), @@ -926,7 +927,7 @@ const frequencyDictionaryRuntime = createFrequencyDictionaryRuntimeService( getFrequencyDictionarySearchPaths, getSourcePath: () => getResolvedConfig().subtitleStyle.frequencyDictionary.sourcePath, setFrequencyRankLookup: (lookup) => { - appState.frequencyRankLookup = lookup as never; + appState.frequencyRankLookup = lookup; }, logInfo: (message) => logger.info(message), })(), @@ -968,7 +969,7 @@ function setFieldGroupingResolver( } const fieldGroupingOverlayRuntime = createFieldGroupingOverlayRuntime( - createBuildFieldGroupingOverlayMainDepsHandler({ + createBuildFieldGroupingOverlayMainDepsHandler({ getMainWindow: () => overlayManager.getMainWindow(), getVisibleOverlayVisible: () => overlayManager.getVisibleOverlayVisible(), getInvisibleOverlayVisible: () => overlayManager.getInvisibleOverlayVisible(), @@ -1257,8 +1258,7 @@ const buildPlayJellyfinItemInMpvMainDepsHandler = createBuildPlayJellyfinItemInM subtitleStreamIndex: params.subtitleStreamIndex ?? undefined, }, ), - applyJellyfinMpvDefaults: (mpvClient) => - applyJellyfinMpvDefaults(mpvClient as unknown as MpvIpcClient), + applyJellyfinMpvDefaults: (mpvClient) => applyJellyfinMpvDefaults(mpvClient), sendMpvCommand: (command) => sendMpvCommandRuntime(appState.mpvClient, command), armQuitOnDisconnect: () => { jellyfinPlayQuitOnDisconnectArmed = false; @@ -2169,10 +2169,7 @@ const { }, }, mpvClientRuntimeServiceFactoryMainDeps: { - createClient: MpvIpcClient as unknown as new ( - socketPath: string, - options: MpvClientRuntimeServiceOptions, - ) => MpvIpcClient, + createClient: MpvIpcClient, getSocketPath: () => appState.mpvSocketPath, getResolvedConfig: () => getResolvedConfig(), isAutoStartOverlayEnabled: () => appState.autoStartOverlay, @@ -2434,7 +2431,7 @@ const { appendToMpvLog, flushMpvLog, showMpvOsd } = createMpvOsdRuntimeHandlers( buildShowMpvOsdMainDeps: (appendToMpvLogHandler) => ({ appendToMpvLog: (message) => appendToMpvLogHandler(message), showMpvOsdRuntime: (mpvClient, text, fallbackLog) => - showMpvOsdRuntime(mpvClient as never, text, fallbackLog), + showMpvOsdRuntime(mpvClient, text, fallbackLog), getMpvClient: () => appState.mpvClient, logInfo: (line) => logger.info(line), }), @@ -2845,7 +2842,7 @@ const { }, createImageFromPath: (iconPath) => nativeImage.createFromPath(iconPath), createEmptyImage: () => nativeImage.createEmpty(), - createTray: (icon) => new Tray(icon as never), + createTray: (icon) => new Tray(icon as ConstructorParameters[0]), trayTooltip: TRAY_TOOLTIP, platform: process.platform, logWarn: (message) => logger.warn(message), @@ -2910,12 +2907,12 @@ const { initializeOverlayRuntime: initializeOverlayRuntimeHandler } = getOverlayWindows: () => getOverlayWindows(), getResolvedConfig: () => getResolvedConfig(), showDesktopNotification, - createFieldGroupingCallback: () => createFieldGroupingCallback() as never, + createFieldGroupingCallback: () => createFieldGroupingCallback(), getKnownWordCacheStatePath: () => path.join(USER_DATA_PATH, 'known-words-cache.json'), }, initializeOverlayRuntimeBootstrapDeps: { isOverlayRuntimeInitialized: () => appState.overlayRuntimeInitialized, - initializeOverlayRuntimeCore: (options) => initializeOverlayRuntimeCore(options as never), + initializeOverlayRuntimeCore, setInvisibleOverlayVisible: (visible) => { overlayManager.setInvisibleOverlayVisible(visible); }, diff --git a/src/main/runtime/app-runtime-main-deps.ts b/src/main/runtime/app-runtime-main-deps.ts index cdac61e..da27598 100644 --- a/src/main/runtime/app-runtime-main-deps.ts +++ b/src/main/runtime/app-runtime-main-deps.ts @@ -1,11 +1,11 @@ -export function createBuildEnsureTrayMainDepsHandler(deps: { - getTray: () => unknown | null; - setTray: (tray: unknown | null) => void; - buildTrayMenu: () => unknown; +export function createBuildEnsureTrayMainDepsHandler(deps: { + getTray: () => TTray | null; + setTray: (tray: TTray | null) => void; + buildTrayMenu: () => TTrayMenu; resolveTrayIconPath: () => string | null; - createImageFromPath: (iconPath: string) => unknown; - createEmptyImage: () => unknown; - createTray: (icon: unknown) => unknown; + createImageFromPath: (iconPath: string) => TTrayIcon; + createEmptyImage: () => TTrayIcon; + createTray: (icon: TTrayIcon) => TTray; trayTooltip: string; platform: string; logWarn: (message: string) => void; @@ -14,13 +14,13 @@ export function createBuildEnsureTrayMainDepsHandler(deps: { setVisibleOverlayVisible: (visible: boolean) => void; }) { return () => ({ - getTray: () => deps.getTray() as never, - setTray: (tray: unknown | null) => deps.setTray(tray), - buildTrayMenu: () => deps.buildTrayMenu() as never, + getTray: () => deps.getTray(), + setTray: (tray: TTray | null) => deps.setTray(tray), + buildTrayMenu: () => deps.buildTrayMenu(), resolveTrayIconPath: () => deps.resolveTrayIconPath(), - createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath) as never, - createEmptyImage: () => deps.createEmptyImage() as never, - createTray: (icon: unknown) => deps.createTray(icon) as never, + createImageFromPath: (iconPath: string) => deps.createImageFromPath(iconPath), + createEmptyImage: () => deps.createEmptyImage(), + createTray: (icon: TTrayIcon) => deps.createTray(icon), trayTooltip: deps.trayTooltip, platform: deps.platform, logWarn: (message: string) => deps.logWarn(message), @@ -33,28 +33,28 @@ export function createBuildEnsureTrayMainDepsHandler(deps: { }); } -export function createBuildDestroyTrayMainDepsHandler(deps: { - getTray: () => unknown | null; - setTray: (tray: unknown | null) => void; +export function createBuildDestroyTrayMainDepsHandler(deps: { + getTray: () => TTray | null; + setTray: (tray: TTray | null) => void; }) { return () => ({ - getTray: () => deps.getTray() as never, - setTray: (tray: unknown | null) => deps.setTray(tray), + getTray: () => deps.getTray(), + setTray: (tray: TTray | null) => deps.setTray(tray), }); } -export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps: { +export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps: { isOverlayRuntimeInitialized: () => boolean; - initializeOverlayRuntimeCore: (options: unknown) => { invisibleOverlayVisible: boolean }; - buildOptions: () => unknown; + initializeOverlayRuntimeCore: (options: TOptions) => { invisibleOverlayVisible: boolean }; + buildOptions: () => TOptions; setInvisibleOverlayVisible: (visible: boolean) => void; setOverlayRuntimeInitialized: (initialized: boolean) => void; startBackgroundWarmups: () => void; }) { return () => ({ isOverlayRuntimeInitialized: () => deps.isOverlayRuntimeInitialized(), - initializeOverlayRuntimeCore: (options: unknown) => deps.initializeOverlayRuntimeCore(options), - buildOptions: () => deps.buildOptions() as never, + initializeOverlayRuntimeCore: (options: TOptions) => deps.initializeOverlayRuntimeCore(options), + buildOptions: () => deps.buildOptions(), setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), setOverlayRuntimeInitialized: (initialized: boolean) => deps.setOverlayRuntimeInitialized(initialized), @@ -62,27 +62,27 @@ export function createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler(deps }); } -export function createBuildOpenYomitanSettingsMainDepsHandler(deps: { - ensureYomitanExtensionLoaded: () => Promise; +export function createBuildOpenYomitanSettingsMainDepsHandler(deps: { + ensureYomitanExtensionLoaded: () => Promise; openYomitanSettingsWindow: (params: { - yomitanExt: unknown; - getExistingWindow: () => unknown | null; - setWindow: (window: unknown | null) => void; + yomitanExt: TYomitanExt; + getExistingWindow: () => TWindow | null; + setWindow: (window: TWindow | null) => void; }) => void; - getExistingWindow: () => unknown | null; - setWindow: (window: unknown | null) => void; + getExistingWindow: () => TWindow | null; + setWindow: (window: TWindow | null) => void; logWarn: (message: string) => void; logError: (message: string, error: unknown) => void; }) { return () => ({ ensureYomitanExtensionLoaded: () => deps.ensureYomitanExtensionLoaded(), openYomitanSettingsWindow: (params: { - yomitanExt: unknown; - getExistingWindow: () => unknown | null; - setWindow: (window: unknown | null) => void; + yomitanExt: TYomitanExt; + getExistingWindow: () => TWindow | null; + setWindow: (window: TWindow | null) => void; }) => deps.openYomitanSettingsWindow(params), getExistingWindow: () => deps.getExistingWindow(), - setWindow: (window: unknown | null) => deps.setWindow(window), + setWindow: (window: TWindow | null) => deps.setWindow(window), logWarn: (message: string) => deps.logWarn(message), logError: (message: string, error: unknown) => deps.logError(message, error), }); diff --git a/src/main/runtime/cli-command-context-factory.test.ts b/src/main/runtime/cli-command-context-factory.test.ts index 654dd7d..584e6c4 100644 --- a/src/main/runtime/cli-command-context-factory.test.ts +++ b/src/main/runtime/cli-command-context-factory.test.ts @@ -6,14 +6,14 @@ test('cli command context factory composes main deps and context handlers', () = const calls: string[] = []; const appState = { mpvSocketPath: '/tmp/mpv.sock', - mpvClient: null as unknown, + mpvClient: null, texthookerPort: 5174, overlayRuntimeInitialized: false, }; const createContext = createCliCommandContextFactory({ appState, - texthookerService: { start: () => null }, + texthookerService: { isRunning: () => false, start: () => null }, getResolvedConfig: () => ({ texthooker: { openBrowser: true } }), openExternal: async () => {}, logBrowserOpenError: () => {}, @@ -32,11 +32,28 @@ test('cli command context factory composes main deps and context handlers', () = triggerFieldGrouping: async () => {}, triggerSubsyncFromConfig: 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: () => {}, openAnilistSetupWindow: () => {}, openJellyfinSetupWindow: () => {}, - getAnilistQueueStatus: () => ({ queued: 0 }), + getAnilistQueueStatus: () => ({ + pending: 0, + ready: 0, + deadLetter: 0, + lastAttemptAt: null, + lastError: null, + }), processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'ok' }), runJellyfinCommand: async () => {}, openYomitanSettings: () => {}, diff --git a/src/main/runtime/cli-command-context-main-deps.test.ts b/src/main/runtime/cli-command-context-main-deps.test.ts index 116706a..7a06eb3 100644 --- a/src/main/runtime/cli-command-context-main-deps.test.ts +++ b/src/main/runtime/cli-command-context-main-deps.test.ts @@ -6,14 +6,14 @@ test('cli command context main deps builder maps state and callbacks', async () const calls: string[] = []; const appState = { mpvSocketPath: '/tmp/mpv.sock', - mpvClient: null as unknown, + mpvClient: null, texthookerPort: 5174, overlayRuntimeInitialized: false, }; const build = createBuildCliCommandContextMainDepsHandler({ appState, - texthookerService: { start: () => null }, + texthookerService: { isRunning: () => false, start: () => null }, getResolvedConfig: () => ({ texthooker: { openBrowser: true } }), openExternal: async (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'); }, - 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'), openAnilistSetupWindow: () => calls.push('open-anilist-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' }), runJellyfinCommand: async () => { calls.push('run-jellyfin'); diff --git a/src/main/runtime/cli-command-context-main-deps.ts b/src/main/runtime/cli-command-context-main-deps.ts index e6204c4..5200fd8 100644 --- a/src/main/runtime/cli-command-context-main-deps.ts +++ b/src/main/runtime/cli-command-context-main-deps.ts @@ -1,13 +1,16 @@ import type { CliArgs } from '../../cli/args'; +import type { CliCommandContextFactoryDeps } from './cli-command-context'; + +type CliCommandContextMainState = { + mpvSocketPath: string; + mpvClient: ReturnType; + texthookerPort: number; + overlayRuntimeInitialized: boolean; +}; export function createBuildCliCommandContextMainDepsHandler(deps: { - appState: { - mpvSocketPath: string; - mpvClient: unknown | null; - texthookerPort: number; - overlayRuntimeInitialized: boolean; - }; - texthookerService: unknown; + appState: CliCommandContextMainState; + texthookerService: CliCommandContextFactoryDeps['texthookerService']; getResolvedConfig: () => { texthooker?: { openBrowser?: boolean } }; openExternal: (url: string) => Promise; logBrowserOpenError: (url: string, error: unknown) => void; @@ -29,12 +32,12 @@ export function createBuildCliCommandContextMainDepsHandler(deps: { triggerSubsyncFromConfig: () => Promise; markLastCardAsAudioCard: () => Promise; - getAnilistStatus: () => unknown; + getAnilistStatus: CliCommandContextFactoryDeps['getAnilistStatus']; clearAnilistToken: () => void; openAnilistSetupWindow: () => void; openJellyfinSetupWindow: () => void; - getAnilistQueueStatus: () => unknown; - processNextAnilistRetryUpdate: () => Promise<{ ok: boolean; message: string }>; + getAnilistQueueStatus: CliCommandContextFactoryDeps['getAnilistQueueStatus']; + processNextAnilistRetryUpdate: CliCommandContextFactoryDeps['retryAnilistQueueNow']; runJellyfinCommand: (args: CliArgs) => Promise; openYomitanSettings: () => void; @@ -49,14 +52,14 @@ export function createBuildCliCommandContextMainDepsHandler(deps: { logWarn: (message: string) => void; logError: (message: string, err: unknown) => void; }) { - return () => ({ + return (): CliCommandContextFactoryDeps => ({ getSocketPath: () => deps.appState.mpvSocketPath, setSocketPath: (socketPath: string) => { deps.appState.mpvSocketPath = socketPath; }, - getMpvClient: () => deps.appState.mpvClient as never, + getMpvClient: () => deps.appState.mpvClient, showOsd: (text: string) => deps.showMpvOsd(text), - texthookerService: deps.texthookerService as never, + texthookerService: deps.texthookerService, getTexthookerPort: () => deps.appState.texthookerPort, setTexthookerPort: (port: number) => { deps.appState.texthookerPort = port; @@ -80,11 +83,11 @@ export function createBuildCliCommandContextMainDepsHandler(deps: { triggerFieldGrouping: () => deps.triggerFieldGrouping(), triggerSubsyncFromConfig: () => deps.triggerSubsyncFromConfig(), markLastCardAsAudioCard: () => deps.markLastCardAsAudioCard(), - getAnilistStatus: () => deps.getAnilistStatus() as never, + getAnilistStatus: () => deps.getAnilistStatus(), clearAnilistToken: () => deps.clearAnilistToken(), openAnilistSetup: () => deps.openAnilistSetupWindow(), openJellyfinSetup: () => deps.openJellyfinSetupWindow(), - getAnilistQueueStatus: () => deps.getAnilistQueueStatus() as never, + getAnilistQueueStatus: () => deps.getAnilistQueueStatus(), retryAnilistQueueNow: () => deps.processNextAnilistRetryUpdate(), runJellyfinCommand: (args: CliArgs) => deps.runJellyfinCommand(args), openYomitanSettings: () => deps.openYomitanSettings(), diff --git a/src/main/runtime/composers/app-ready-composer.test.ts b/src/main/runtime/composers/app-ready-composer.test.ts index acc048a..471ab27 100644 --- a/src/main/runtime/composers/app-ready-composer.test.ts +++ b/src/main/runtime/composers/app-ready-composer.test.ts @@ -5,7 +5,7 @@ import { composeAppReadyRuntime } from './app-ready-composer'; test('composeAppReadyRuntime returns reload/critical/app-ready handlers', () => { const composed = composeAppReadyRuntime({ reloadConfigMainDeps: { - reloadConfigStrict: () => ({ config: {} as never, warnings: [] }), + reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }), logInfo: () => {}, logWarning: () => {}, showDesktopNotification: () => {}, diff --git a/src/main/runtime/composers/mpv-runtime-composer.test.ts b/src/main/runtime/composers/mpv-runtime-composer.test.ts index fb49466..ea0a503 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.test.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.test.ts @@ -85,7 +85,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject mpvClientRuntimeServiceFactoryMainDeps: { createClient: FakeMpvClient, getSocketPath: () => '/tmp/mpv.sock', - getResolvedConfig: () => ({}) as never, + getResolvedConfig: () => ({ auto_start_overlay: false }), isAutoStartOverlayEnabled: () => true, setOverlayVisible: () => {}, shouldBindVisibleOverlayToMpvSubVisibility: () => true, @@ -118,7 +118,7 @@ test('composeMpvRuntimeHandlers returns callable handlers and forwards to inject setYomitanParserInitPromise: () => {}, isKnownWord: (text) => text === 'known', recordLookup: () => {}, - getKnownWordMatchMode: () => 'exact', + getKnownWordMatchMode: () => 'headword', getMinSentenceWordsForNPlusOne: () => 3, getJlptLevel: () => null, getJlptEnabled: () => true, diff --git a/src/main/runtime/composers/mpv-runtime-composer.ts b/src/main/runtime/composers/mpv-runtime-composer.ts index 0fbf150..9093d6e 100644 --- a/src/main/runtime/composers/mpv-runtime-composer.ts +++ b/src/main/runtime/composers/mpv-runtime-composer.ts @@ -3,6 +3,7 @@ import { createBuildBindMpvMainEventHandlersMainDepsHandler } from '../mpv-main- import { createBuildMpvClientRuntimeServiceFactoryDepsHandler } from '../mpv-client-runtime-service-main-deps'; import { createMpvClientRuntimeServiceFactory } 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 { createUpdateMpvSubtitleRenderMetricsHandler } from '../mpv-subtitle-render-metrics'; import { @@ -30,7 +31,7 @@ type MpvClientRuntimeServiceFactoryMainDeps Parameters< typeof createBuildMpvClientRuntimeServiceFactoryDepsHandler< TMpvClient, - unknown, + Config, MpvClientRuntimeServiceOptions > >[0], @@ -107,7 +108,7 @@ export function composeMpvRuntimeHandlers< const buildMpvClientRuntimeServiceFactoryMainDepsHandler = createBuildMpvClientRuntimeServiceFactoryDepsHandler< TMpvClient, - unknown, + Config, MpvClientRuntimeServiceOptions >({ ...options.mpvClientRuntimeServiceFactoryMainDeps, diff --git a/src/main/runtime/dictionary-runtime-main-deps.ts b/src/main/runtime/dictionary-runtime-main-deps.ts index 0d73769..b64ba5a 100644 --- a/src/main/runtime/dictionary-runtime-main-deps.ts +++ b/src/main/runtime/dictionary-runtime-main-deps.ts @@ -1,3 +1,7 @@ +import type { FrequencyDictionaryLookup, JlptLevel } from '../../types'; + +type JlptLookup = (term: string) => JlptLevel | null; + export function createBuildDictionaryRootsMainHandler(deps: { dirname: string; appPath: string; @@ -8,20 +12,19 @@ export function createBuildDictionaryRootsMainHandler(deps: { cwd: string; joinPath: (...parts: string[]) => string; }) { - return () => - [ - deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'), - deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'), - deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'), - deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'), - deps.userDataPath, - deps.appUserDataPath, - 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.cwd, - ]; + return () => [ + deps.joinPath(deps.dirname, '..', '..', 'vendor', 'yomitan-jlpt-vocab'), + deps.joinPath(deps.appPath, 'vendor', 'yomitan-jlpt-vocab'), + deps.joinPath(deps.resourcesPath, 'yomitan-jlpt-vocab'), + deps.joinPath(deps.resourcesPath, 'app.asar', 'vendor', 'yomitan-jlpt-vocab'), + deps.userDataPath, + deps.appUserDataPath, + 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.cwd, + ]; } export function createBuildFrequencyDictionaryRootsMainHandler(deps: { @@ -57,7 +60,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: { isJlptEnabled: () => boolean; getDictionaryRoots: () => string[]; getJlptDictionarySearchPaths: (deps: { getDictionaryRoots: () => string[] }) => string[]; - setJlptLevelLookup: (lookup: unknown) => void; + setJlptLevelLookup: (lookup: JlptLookup) => void; logInfo: (message: string) => void; }) { return () => ({ @@ -66,7 +69,7 @@ export function createBuildJlptDictionaryRuntimeMainDepsHandler(deps: { deps.getJlptDictionarySearchPaths({ getDictionaryRoots: () => deps.getDictionaryRoots(), }), - setJlptLevelLookup: (lookup: unknown) => deps.setJlptLevelLookup(lookup), + setJlptLevelLookup: (lookup: JlptLookup) => deps.setJlptLevelLookup(lookup), log: (message: string) => deps.logInfo(`[JLPT] ${message}`), }); } @@ -79,17 +82,19 @@ export function createBuildFrequencyDictionaryRuntimeMainDepsHandler(deps: { getSourcePath: () => string | undefined; }) => string[]; getSourcePath: () => string | undefined; - setFrequencyRankLookup: (lookup: unknown) => void; + setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => void; logInfo: (message: string) => void; }) { return () => ({ isFrequencyDictionaryEnabled: () => deps.isFrequencyDictionaryEnabled(), getSearchPaths: () => deps.getFrequencyDictionarySearchPaths({ - getDictionaryRoots: () => deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot), + getDictionaryRoots: () => + deps.getDictionaryRoots().filter((dictionaryRoot) => dictionaryRoot), getSourcePath: () => deps.getSourcePath(), }), - setFrequencyRankLookup: (lookup: unknown) => deps.setFrequencyRankLookup(lookup), + setFrequencyRankLookup: (lookup: FrequencyDictionaryLookup) => + deps.setFrequencyRankLookup(lookup), log: (message: string) => deps.logInfo(`[Frequency] ${message}`), }); } diff --git a/src/main/runtime/field-grouping-overlay-main-deps.test.ts b/src/main/runtime/field-grouping-overlay-main-deps.test.ts index 3fde0c6..4fac2e6 100644 --- a/src/main/runtime/field-grouping-overlay-main-deps.test.ts +++ b/src/main/runtime/field-grouping-overlay-main-deps.test.ts @@ -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 deps = createBuildFieldGroupingOverlayMainDepsHandler({ - getMainWindow: () => ({ id: 'main' }), + getMainWindow: () => ({ + isDestroyed: () => false, + webContents: { + send: () => {}, + }, + }), getVisibleOverlayVisible: () => true, getInvisibleOverlayVisible: () => false, 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.getInvisibleOverlayVisible(), false); assert.equal(deps.getResolver(), resolver); diff --git a/src/main/runtime/field-grouping-overlay-main-deps.ts b/src/main/runtime/field-grouping-overlay-main-deps.ts index 1edd8de..9f3de0a 100644 --- a/src/main/runtime/field-grouping-overlay-main-deps.ts +++ b/src/main/runtime/field-grouping-overlay-main-deps.ts @@ -1,29 +1,34 @@ -export function createBuildFieldGroupingOverlayMainDepsHandler< - TModal extends string, - TChoice, ->(deps: { - getMainWindow: () => unknown | null; - 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; +import type { FieldGroupingOverlayRuntimeOptions } from '../../core/services/field-grouping-overlay'; + +type FieldGroupingOverlayMainDeps = Omit< + FieldGroupingOverlayRuntimeOptions, + 'sendToVisibleOverlay' +> & { sendToActiveOverlayWindow: ( channel: string, payload?: unknown, runtimeOptions?: { restoreOnModalClose?: TModal }, ) => boolean; -}) { - return () => ({ - getMainWindow: () => deps.getMainWindow() as never, +}; + +type BuiltFieldGroupingOverlayMainDeps = + FieldGroupingOverlayRuntimeOptions & { + sendToVisibleOverlay: NonNullable< + FieldGroupingOverlayRuntimeOptions['sendToVisibleOverlay'] + >; + }; + +export function createBuildFieldGroupingOverlayMainDepsHandler( + deps: FieldGroupingOverlayMainDeps, +) { + return (): BuiltFieldGroupingOverlayMainDeps => ({ + getMainWindow: () => deps.getMainWindow(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), setVisibleOverlayVisible: (visible: boolean) => deps.setVisibleOverlayVisible(visible), setInvisibleOverlayVisible: (visible: boolean) => deps.setInvisibleOverlayVisible(visible), - getResolver: () => deps.getResolver() as never, - setResolver: (resolver: ((choice: TChoice) => void) | null) => deps.setResolver(resolver), + getResolver: () => deps.getResolver(), + setResolver: (resolver) => deps.setResolver(resolver), getRestoreVisibleOverlayOnModalClose: () => deps.getRestoreVisibleOverlayOnModalClose(), sendToVisibleOverlay: ( channel: string, diff --git a/src/main/runtime/jellyfin-client-info.ts b/src/main/runtime/jellyfin-client-info.ts index 80ef052..24ce367 100644 --- a/src/main/runtime/jellyfin-client-info.ts +++ b/src/main/runtime/jellyfin-client-info.ts @@ -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: { - getResolvedConfig: () => { jellyfin: unknown }; - loadStoredSession: () => { accessToken: string; userId: string } | null | undefined; + getResolvedConfig: () => { jellyfin: ResolvedJellyfinConfig }; + loadStoredSession: () => JellyfinStoredSession | null | undefined; getEnv: (name: string) => string | undefined; }) { - return () => { - const jellyfin = deps.getResolvedConfig().jellyfin as { - userId?: string; - [key: string]: unknown; - }; + return (): ResolvedJellyfinConfigWithSession => { + const jellyfin = deps.getResolvedConfig().jellyfin; const envToken = deps.getEnv('SUBMINER_JELLYFIN_ACCESS_TOKEN')?.trim() ?? ''; const envUserId = deps.getEnv('SUBMINER_JELLYFIN_USER_ID')?.trim() ?? ''; @@ -20,7 +26,7 @@ export function createGetResolvedJellyfinConfigHandler(deps: { ...jellyfin, accessToken: envToken, userId: envUserId || storedUserId || '', - } as never; + }; } if (storedToken.length > 0 && storedUserId.length > 0) { @@ -28,24 +34,20 @@ export function createGetResolvedJellyfinConfigHandler(deps: { ...jellyfin, accessToken: storedToken, userId: storedUserId, - } as never; + }; } - return jellyfin as never; + return jellyfin; }; } export function createGetJellyfinClientInfoHandler(deps: { - getResolvedJellyfinConfig: () => { - clientName?: string; - clientVersion?: string; - deviceId?: string; - }; - getDefaultJellyfinConfig: () => { - clientName?: string; - clientVersion?: string; - deviceId?: string; - }; + getResolvedJellyfinConfig: () => Partial< + Pick + >; + getDefaultJellyfinConfig: () => Partial< + Pick + >; }) { return ( config = deps.getResolvedJellyfinConfig(), diff --git a/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts b/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts index a8ab37e..0b1a98e 100644 --- a/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts +++ b/src/main/runtime/jellyfin-playback-launch-main-deps.test.ts @@ -6,12 +6,14 @@ test('play jellyfin item in mpv main deps builder maps callbacks', async () => { const calls: string[] = []; const deps = createBuildPlayJellyfinItemInMpvMainDepsHandler({ ensureMpvConnectedForPlayback: async () => true, - getMpvClient: () => ({ connected: true }), + getMpvClient: () => ({ connected: true, send: () => {} }), resolvePlaybackPlan: async () => ({ url: 'u', mode: 'direct', title: 't', startTimeTicks: 0, + audioStreamIndex: null, + subtitleStreamIndex: null, }), applyJellyfinMpvDefaults: () => calls.push('defaults'), 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(typeof deps.getMpvClient(), 'object'); assert.deepEqual( - await deps.resolvePlaybackPlan({ session: {} as never, clientInfo: {} as never, jellyfinConfig: {}, itemId: 'i' }), - { url: 'u', mode: 'direct', title: 't', startTimeTicks: 0 }, + await deps.resolvePlaybackPlan({ + 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.armQuitOnDisconnect(); deps.schedule(() => {}, 500); 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.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'); assert.deepEqual(calls, [ diff --git a/src/main/runtime/jellyfin-playback-launch.test.ts b/src/main/runtime/jellyfin-playback-launch.test.ts index c5decfc..5aa8fe4 100644 --- a/src/main/runtime/jellyfin-playback-launch.test.ts +++ b/src/main/runtime/jellyfin-playback-launch.test.ts @@ -54,7 +54,7 @@ test('playback handler drives mpv commands and playback state', async () => { const reportPayloads: Array> = []; const handler = createPlayJellyfinItemInMpvHandler({ ensureMpvConnectedForPlayback: async () => true, - getMpvClient: () => ({ connected: true }), + getMpvClient: () => ({ connected: true, send: () => {} }), resolvePlaybackPlan: async () => ({ url: 'https://stream.example/video.m3u8', mode: 'direct', diff --git a/src/main/runtime/jellyfin-playback-launch.ts b/src/main/runtime/jellyfin-playback-launch.ts index 402e7b7..915106d 100644 --- a/src/main/runtime/jellyfin-playback-launch.ts +++ b/src/main/runtime/jellyfin-playback-launch.ts @@ -1,9 +1,6 @@ -type JellyfinSession = { - serverUrl: string; - accessToken: string; - userId: string; - username: string; -}; +import type { JellyfinAuthSession, JellyfinPlaybackPlan } from '../../core/services/jellyfin'; +import type { JellyfinConfig } from '../../types'; +import type { MpvRuntimeClientLike } from '../../core/services/mpv'; type JellyfinClientInfo = { clientName: string; @@ -11,15 +8,6 @@ type JellyfinClientInfo = { deviceId: string; }; -type JellyfinPlaybackPlan = { - url: string; - mode: 'direct' | 'transcode'; - title: string; - startTimeTicks: number; - audioStreamIndex?: number | null; - subtitleStreamIndex?: number | null; -}; - type ActivePlaybackState = { itemId: string; mediaSourceId: undefined; @@ -28,26 +16,24 @@ type ActivePlaybackState = { playMethod: 'DirectPlay' | 'Transcode'; }; -type MpvClientLike = unknown; - export function createPlayJellyfinItemInMpvHandler(deps: { ensureMpvConnectedForPlayback: () => Promise; - getMpvClient: () => MpvClientLike | null; + getMpvClient: () => MpvRuntimeClientLike | null; resolvePlaybackPlan: (params: { - session: JellyfinSession; + session: JellyfinAuthSession; clientInfo: JellyfinClientInfo; - jellyfinConfig: unknown; + jellyfinConfig: JellyfinConfig; itemId: string; audioStreamIndex?: number | null; subtitleStreamIndex?: number | null; }) => Promise; - applyJellyfinMpvDefaults: (mpvClient: MpvClientLike) => void; + applyJellyfinMpvDefaults: (mpvClient: MpvRuntimeClientLike) => void; sendMpvCommand: (command: Array) => void; armQuitOnDisconnect: () => void; schedule: (callback: () => void, delayMs: number) => void; convertTicksToSeconds: (ticks: number) => number; preloadExternalSubtitles: (params: { - session: JellyfinSession; + session: JellyfinAuthSession; clientInfo: JellyfinClientInfo; itemId: string; }) => void; @@ -64,9 +50,9 @@ export function createPlayJellyfinItemInMpvHandler(deps: { showMpvOsd: (text: string) => void; }) { return async (params: { - session: JellyfinSession; + session: JellyfinAuthSession; clientInfo: JellyfinClientInfo; - jellyfinConfig: unknown; + jellyfinConfig: JellyfinConfig; itemId: string; audioStreamIndex?: number | null; subtitleStreamIndex?: number | null; @@ -96,7 +82,11 @@ export function createPlayJellyfinItemInMpvHandler(deps: { if (params.setQuitOnDisconnectArm !== false) { 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.schedule(() => { deps.sendMpvCommand(['set_property', 'sid', 'no']); diff --git a/src/main/runtime/mpv-client-runtime-service.ts b/src/main/runtime/mpv-client-runtime-service.ts index 2b94fd2..3f6056f 100644 --- a/src/main/runtime/mpv-client-runtime-service.ts +++ b/src/main/runtime/mpv-client-runtime-service.ts @@ -1,5 +1,7 @@ +import type { Config } from '../../types'; + export type MpvClientRuntimeServiceOptions = { - getResolvedConfig: () => unknown; + getResolvedConfig: () => Config; autoStartOverlay: boolean; setOverlayVisible: (visible: boolean) => void; shouldBindVisibleOverlayToMpvSubVisibility: () => boolean; diff --git a/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts b/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts index 301f16d..cb2d36c 100644 --- a/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts +++ b/src/main/runtime/mpv-jellyfin-defaults-main-deps.test.ts @@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults main deps builder maps callbacks', () => { 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.deepEqual(calls, ['set_property:aid:auto']); }); diff --git a/src/main/runtime/mpv-jellyfin-defaults.test.ts b/src/main/runtime/mpv-jellyfin-defaults.test.ts index 06e4d08..6f2ea8e 100644 --- a/src/main/runtime/mpv-jellyfin-defaults.test.ts +++ b/src/main/runtime/mpv-jellyfin-defaults.test.ts @@ -12,7 +12,7 @@ test('apply jellyfin mpv defaults sends expected property commands', () => { jellyfinLangPref: 'ja,jp', }); - applyDefaults({}); + applyDefaults({ connected: true, send: () => {} }); assert.deepEqual(calls, [ 'set_property:sub-auto:fuzzy', 'set_property:aid:auto', diff --git a/src/main/runtime/mpv-jellyfin-defaults.ts b/src/main/runtime/mpv-jellyfin-defaults.ts index b1a1d60..6ee1247 100644 --- a/src/main/runtime/mpv-jellyfin-defaults.ts +++ b/src/main/runtime/mpv-jellyfin-defaults.ts @@ -1,13 +1,10 @@ -type MpvClientLike = unknown; +import type { MpvRuntimeClientLike } from '../../core/services/mpv'; export function createApplyJellyfinMpvDefaultsHandler(deps: { - sendMpvCommandRuntime: ( - client: MpvClientLike, - command: [string, string, string], - ) => void; + sendMpvCommandRuntime: (client: MpvRuntimeClientLike, command: [string, string, string]) => void; jellyfinLangPref: string; }) { - return (client: MpvClientLike): void => { + return (client: MpvRuntimeClientLike): void => { deps.sendMpvCommandRuntime(client, ['set_property', 'sub-auto', 'fuzzy']); deps.sendMpvCommandRuntime(client, ['set_property', 'aid', 'auto']); deps.sendMpvCommandRuntime(client, ['set_property', 'sid', 'auto']); @@ -18,9 +15,7 @@ export function createApplyJellyfinMpvDefaultsHandler(deps: { }; } -export function createGetDefaultSocketPathHandler(deps: { - platform: string; -}) { +export function createGetDefaultSocketPathHandler(deps: { platform: string }) { return (): string => { if (deps.platform === 'win32') { return '\\\\.\\pipe\\subminer-socket'; diff --git a/src/main/runtime/mpv-main-event-bindings.ts b/src/main/runtime/mpv-main-event-bindings.ts index 6957e57..8c5239d 100644 --- a/src/main/runtime/mpv-main-event-bindings.ts +++ b/src/main/runtime/mpv-main-event-bindings.ts @@ -15,9 +15,7 @@ import { createHandleMpvTimePosChangeHandler, } from './mpv-main-event-actions'; -type MpvEventClient = { - on: (...args: any[]) => unknown; -}; +type MpvEventClient = Parameters>[0]; export function createBindMpvMainEventHandlersHandler(deps: { reportJellyfinRemoteStopped: () => void; @@ -119,7 +117,8 @@ export function createBindMpvMainEventHandlersHandler(deps: { updateSubtitleRenderMetrics: (patch) => deps.updateSubtitleRenderMetrics(patch), }); const handleMpvSecondarySubtitleVisibility = createHandleMpvSecondarySubtitleVisibilityHandler({ - setPreviousSecondarySubVisibility: (visible) => deps.setPreviousSecondarySubVisibility(visible), + setPreviousSecondarySubVisibility: (visible) => + deps.setPreviousSecondarySubVisibility(visible), }); createBindMpvClientEventHandlers({ @@ -134,6 +133,6 @@ export function createBindMpvMainEventHandlersHandler(deps: { onPauseChange: handleMpvPauseChange, onSubtitleMetricsChange: handleMpvSubtitleMetricsChange, onSecondarySubtitleVisibility: handleMpvSecondarySubtitleVisibility, - })(mpvClient as never); + })(mpvClient); }; } diff --git a/src/main/runtime/mpv-osd-log-main-deps.test.ts b/src/main/runtime/mpv-osd-log-main-deps.test.ts index a7fc813..de5131d 100644 --- a/src/main/runtime/mpv-osd-log-main-deps.test.ts +++ b/src/main/runtime/mpv-osd-log-main-deps.test.ts @@ -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', () => { const calls: string[] = []; - const client = { id: 'mpv' }; + const client = { + connected: true, + send: () => {}, + }; const deps = createBuildShowMpvOsdMainDepsHandler({ appendToMpvLog: (message) => calls.push(`append:${message}`), showMpvOsdRuntime: (_mpvClient, text, fallbackLog) => { diff --git a/src/main/runtime/mpv-osd-log-main-deps.ts b/src/main/runtime/mpv-osd-log-main-deps.ts index c543e3d..c3816d0 100644 --- a/src/main/runtime/mpv-osd-log-main-deps.ts +++ b/src/main/runtime/mpv-osd-log-main-deps.ts @@ -1,11 +1,10 @@ -export function createBuildAppendToMpvLogMainDepsHandler(deps: { - logPath: string; - dirname: (targetPath: string) => string; - mkdir: (targetPath: string, options: { recursive: boolean }) => Promise; - appendFile: (targetPath: string, data: string, options: { encoding: 'utf8' }) => Promise; - now: () => Date; -}) { - return () => ({ +import type { createAppendToMpvLogHandler, createShowMpvOsdHandler } from './mpv-osd-log'; + +type AppendToMpvLogMainDeps = Parameters[0]; +type ShowMpvOsdMainDeps = Parameters[0]; + +export function createBuildAppendToMpvLogMainDepsHandler(deps: AppendToMpvLogMainDeps) { + return (): AppendToMpvLogMainDeps => ({ logPath: deps.logPath, dirname: (targetPath: string) => deps.dirname(targetPath), mkdir: (targetPath: string, options: { recursive: boolean }) => deps.mkdir(targetPath, options), @@ -15,24 +14,12 @@ export function createBuildAppendToMpvLogMainDepsHandler(deps: { }); } -export function createBuildShowMpvOsdMainDepsHandler(deps: { - appendToMpvLog: (message: string) => void; - showMpvOsdRuntime: ( - mpvClient: unknown | null, - text: string, - fallbackLog: (line: string) => void, - ) => void; - getMpvClient: () => unknown | null; - logInfo: (line: string) => void; -}) { - return () => ({ +export function createBuildShowMpvOsdMainDepsHandler(deps: ShowMpvOsdMainDeps) { + return (): ShowMpvOsdMainDeps => ({ appendToMpvLog: (message: string) => deps.appendToMpvLog(message), - showMpvOsdRuntime: ( - mpvClient: unknown | null, - text: string, - fallbackLog: (line: string) => void, - ) => deps.showMpvOsdRuntime(mpvClient, text, fallbackLog), - getMpvClient: () => deps.getMpvClient() as never, + showMpvOsdRuntime: (mpvClient, text, fallbackLog) => + deps.showMpvOsdRuntime(mpvClient, text, fallbackLog), + getMpvClient: () => deps.getMpvClient(), logInfo: (line: string) => deps.logInfo(line), }); } diff --git a/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts b/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts index d051e92..8ce8462 100644 --- a/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts +++ b/src/main/runtime/overlay-runtime-bootstrap-handlers.test.ts @@ -1,15 +1,17 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import type { BaseWindowTracker } from '../../window-trackers'; +import type { KikuFieldGroupingChoice } from '../../types'; import { createOverlayRuntimeBootstrapHandlers } from './overlay-runtime-bootstrap-handlers'; test('overlay runtime bootstrap handlers compose options builder and bootstrap handler', () => { const appState = { backendOverride: null as string | null, - windowTracker: null as unknown, + windowTracker: null as BaseWindowTracker | null, subtitleTimingTracker: null as unknown, - mpvClient: null as unknown, + mpvClient: null, mpvSocketPath: '/tmp/mpv.sock', - runtimeOptionsManager: null as unknown, + runtimeOptionsManager: null, ankiIntegration: null as unknown, }; let initialized = false; @@ -39,7 +41,13 @@ test('overlay runtime bootstrap handlers compose options builder and bootstrap h getOverlayWindows: () => [], getResolvedConfig: () => ({}), showDesktopNotification: () => {}, - createFieldGroupingCallback: () => (async () => 'combined' as never), + createFieldGroupingCallback: () => async () => + ({ + keepNoteId: 1, + deleteNoteId: 2, + deleteDuplicate: false, + cancelled: true, + }) as KikuFieldGroupingChoice, getKnownWordCacheStatePath: () => '/tmp/known.json', }, initializeOverlayRuntimeBootstrapDeps: { diff --git a/src/main/runtime/overlay-runtime-bootstrap-handlers.ts b/src/main/runtime/overlay-runtime-bootstrap-handlers.ts index 0e5d220..21cbe43 100644 --- a/src/main/runtime/overlay-runtime-bootstrap-handlers.ts +++ b/src/main/runtime/overlay-runtime-bootstrap-handlers.ts @@ -6,17 +6,24 @@ import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-ru type InitializeOverlayRuntimeMainDeps = Parameters< typeof createBuildInitializeOverlayRuntimeMainDepsHandler >[0]; +type InitializeOverlayRuntimeOptions = ReturnType< + ReturnType +>; type InitializeOverlayRuntimeBootstrapMainDeps = Parameters< - typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler + typeof createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler >[0]; export function createOverlayRuntimeBootstrapHandlers(deps: { initializeOverlayRuntimeMainDeps: InitializeOverlayRuntimeMainDeps; - initializeOverlayRuntimeBootstrapDeps: Omit; + initializeOverlayRuntimeBootstrapDeps: Omit< + InitializeOverlayRuntimeBootstrapMainDeps, + 'buildOptions' + >; }) { - const buildInitializeOverlayRuntimeOptionsHandler = createBuildInitializeOverlayRuntimeOptionsHandler( - createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(), - ); + const buildInitializeOverlayRuntimeOptionsHandler = + createBuildInitializeOverlayRuntimeOptionsHandler( + createBuildInitializeOverlayRuntimeMainDepsHandler(deps.initializeOverlayRuntimeMainDeps)(), + ); const initializeOverlayRuntime = createInitializeOverlayRuntimeHandler( createBuildInitializeOverlayRuntimeBootstrapMainDepsHandler({ ...deps.initializeOverlayRuntimeBootstrapDeps, diff --git a/src/main/runtime/overlay-runtime-options-main-deps.test.ts b/src/main/runtime/overlay-runtime-options-main-deps.test.ts index e5bc63e..8e15506 100644 --- a/src/main/runtime/overlay-runtime-options-main-deps.test.ts +++ b/src/main/runtime/overlay-runtime-options-main-deps.test.ts @@ -1,12 +1,13 @@ import assert from 'node:assert/strict'; import test from 'node:test'; +import type { BaseWindowTracker } from '../../window-trackers'; import { createBuildInitializeOverlayRuntimeMainDepsHandler } from './overlay-runtime-options-main-deps'; test('overlay runtime main deps builder maps runtime state and callbacks', () => { const calls: string[] = []; const appState = { backendOverride: 'x11' as string | null, - windowTracker: null as unknown, + windowTracker: null as BaseWindowTracker | null, subtitleTimingTracker: { id: 'tracker' } as unknown, mpvClient: null as { send?: (payload: { command: string[] }) => void } | null, mpvSocketPath: '/tmp/mpv.sock', @@ -36,7 +37,12 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () => getOverlayWindows: () => [], getResolvedConfig: () => ({}), showDesktopNotification: () => calls.push('notify'), - createFieldGroupingCallback: () => async () => ({ cancelled: true }), + createFieldGroupingCallback: () => async () => ({ + keepNoteId: 1, + deleteNoteId: 2, + deleteDuplicate: false, + cancelled: true, + }), getKnownWordCacheStatePath: () => '/tmp/known-words-cache.json', }); @@ -58,7 +64,11 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () => deps.syncOverlayShortcuts(); deps.showDesktopNotification('title', {}); - deps.setWindowTracker({ id: 'tracker' }); + const tracker = { + close: () => {}, + getWindowGeometry: () => null, + } as unknown as BaseWindowTracker; + deps.setWindowTracker(tracker); deps.setAnkiIntegration({ id: 'anki' }); assert.deepEqual(calls, [ @@ -72,6 +82,6 @@ test('overlay runtime main deps builder maps runtime state and callbacks', () => 'sync-shortcuts', 'notify', ]); - assert.deepEqual(appState.windowTracker, { id: 'tracker' }); + assert.equal(appState.windowTracker, tracker); assert.deepEqual(appState.ankiIntegration, { id: 'anki' }); }); diff --git a/src/main/runtime/overlay-runtime-options-main-deps.ts b/src/main/runtime/overlay-runtime-options-main-deps.ts index 5b1595a..b088d1b 100644 --- a/src/main/runtime/overlay-runtime-options-main-deps.ts +++ b/src/main/runtime/overlay-runtime-options-main-deps.ts @@ -1,14 +1,19 @@ import type { AnkiConnectConfig } from '../../types'; +import type { createBuildInitializeOverlayRuntimeOptionsHandler } from './overlay-runtime-options'; + +type OverlayRuntimeOptionsMainDeps = Parameters< + typeof createBuildInitializeOverlayRuntimeOptionsHandler +>[0]; export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { appState: { backendOverride: string | null; - windowTracker: unknown | null; - subtitleTimingTracker: unknown | null; - mpvClient: unknown | null; + windowTracker: Parameters[0]; + subtitleTimingTracker: ReturnType; + mpvClient: ReturnType; mpvSocketPath: string; - runtimeOptionsManager: unknown | null; - ankiIntegration: unknown | null; + runtimeOptionsManager: ReturnType; + ankiIntegration: Parameters[0]; }; overlayManager: { getVisibleOverlayVisible: () => boolean; @@ -25,27 +30,36 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { createMainWindow: () => void; createInvisibleWindow: () => 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: { x: number; y: number; width: number; height: number; }) => void; - getOverlayWindows: () => unknown[]; + getOverlayWindows: OverlayRuntimeOptionsMainDeps['getOverlayWindows']; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; showDesktopNotification: (title: string, options: { body?: string; icon?: string }) => void; - createFieldGroupingCallback: () => unknown; + createFieldGroupingCallback: OverlayRuntimeOptionsMainDeps['createFieldGroupingCallback']; getKnownWordCacheStatePath: () => string; }) { - return () => ({ + return (): OverlayRuntimeOptionsMainDeps => ({ getBackendOverride: () => deps.appState.backendOverride, getInitialInvisibleOverlayVisibility: () => deps.getInitialInvisibleOverlayVisibility(), createMainWindow: () => deps.createMainWindow(), createInvisibleWindow: () => deps.createInvisibleWindow(), registerGlobalShortcuts: () => deps.registerGlobalShortcuts(), - updateVisibleOverlayBounds: (geometry: { x: number; y: number; width: number; height: number }) => - deps.updateVisibleOverlayBounds(geometry), + updateVisibleOverlayBounds: (geometry: { + x: number; + y: number; + width: number; + height: number; + }) => deps.updateVisibleOverlayBounds(geometry), updateInvisibleOverlayBounds: (geometry: { x: number; y: number; @@ -54,28 +68,25 @@ export function createBuildInitializeOverlayRuntimeMainDepsHandler(deps: { }) => deps.updateInvisibleOverlayBounds(geometry), isVisibleOverlayVisible: () => deps.overlayManager.getVisibleOverlayVisible(), isInvisibleOverlayVisible: () => deps.overlayManager.getInvisibleOverlayVisible(), - updateVisibleOverlayVisibility: () => deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), + updateVisibleOverlayVisibility: () => + deps.overlayVisibilityRuntime.updateVisibleOverlayVisibility(), updateInvisibleOverlayVisibility: () => deps.overlayVisibilityRuntime.updateInvisibleOverlayVisibility(), - getOverlayWindows: () => deps.getOverlayWindows() as never, + getOverlayWindows: () => deps.getOverlayWindows(), syncOverlayShortcuts: () => deps.overlayShortcutsRuntime.syncOverlayShortcuts(), - setWindowTracker: (tracker: unknown | null) => { + setWindowTracker: (tracker) => { deps.appState.windowTracker = tracker; }, getResolvedConfig: () => deps.getResolvedConfig(), getSubtitleTimingTracker: () => deps.appState.subtitleTimingTracker, - getMpvClient: () => - (deps.appState.mpvClient as { send?: (payload: { command: string[] }) => void } | null), + getMpvClient: () => deps.appState.mpvClient, getMpvSocketPath: () => deps.appState.mpvSocketPath, - getRuntimeOptionsManager: () => - deps.appState.runtimeOptionsManager as - | { getEffectiveAnkiConnectConfig: (config?: AnkiConnectConfig) => AnkiConnectConfig } - | null, - setAnkiIntegration: (integration: unknown | null) => { + getRuntimeOptionsManager: () => deps.appState.runtimeOptionsManager, + setAnkiIntegration: (integration) => { deps.appState.ankiIntegration = integration; }, showDesktopNotification: deps.showDesktopNotification, - createFieldGroupingCallback: () => deps.createFieldGroupingCallback() as never, + createFieldGroupingCallback: () => deps.createFieldGroupingCallback(), getKnownWordCacheStatePath: () => deps.getKnownWordCacheStatePath(), }); } diff --git a/src/main/runtime/overlay-runtime-options.ts b/src/main/runtime/overlay-runtime-options.ts index c953294..a6873a4 100644 --- a/src/main/runtime/overlay-runtime-options.ts +++ b/src/main/runtime/overlay-runtime-options.ts @@ -5,6 +5,7 @@ import type { WindowGeometry, } from '../../types'; import type { BrowserWindow } from 'electron'; +import type { BaseWindowTracker } from '../../window-trackers'; type OverlayRuntimeOptions = { backendOverride: string | null; @@ -20,7 +21,7 @@ type OverlayRuntimeOptions = { updateInvisibleOverlayVisibility: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; - setWindowTracker: (tracker: unknown | null) => void; + setWindowTracker: (tracker: BaseWindowTracker | null) => void; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; getSubtitleTimingTracker: () => unknown | null; getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null; @@ -50,7 +51,7 @@ export function createBuildInitializeOverlayRuntimeOptionsHandler(deps: { updateInvisibleOverlayVisibility: () => void; getOverlayWindows: () => BrowserWindow[]; syncOverlayShortcuts: () => void; - setWindowTracker: (tracker: unknown | null) => void; + setWindowTracker: (tracker: BaseWindowTracker | null) => void; getResolvedConfig: () => { ankiConnect?: AnkiConnectConfig }; getSubtitleTimingTracker: () => unknown | null; getMpvClient: () => { send?: (payload: { command: string[] }) => void } | null; diff --git a/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts b/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts index ecf6e5f..37041ec 100644 --- a/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts +++ b/src/main/runtime/overlay-visibility-runtime-main-deps.test.ts @@ -1,3 +1,5 @@ +import type { BaseWindowTracker } from '../../window-trackers'; + import assert from 'node:assert/strict'; import test from 'node:test'; 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; const mainWindow = { id: 'main' } as never; const invisibleWindow = { id: 'invisible' } as never; + const tracker = { id: 'tracker' } as unknown as BaseWindowTracker; const deps = createBuildOverlayVisibilityRuntimeMainDepsHandler({ getMainWindow: () => mainWindow, getInvisibleWindow: () => invisibleWindow, getVisibleOverlayVisible: () => true, getInvisibleOverlayVisible: () => false, - getWindowTracker: () => ({ id: 'tracker' }), + getWindowTracker: () => tracker, getTrackerNotReadyWarningShown: () => trackerNotReadyWarningShown, setTrackerNotReadyWarningShown: (shown) => { trackerNotReadyWarningShown = shown; diff --git a/src/main/runtime/overlay-visibility-runtime-main-deps.ts b/src/main/runtime/overlay-visibility-runtime-main-deps.ts index 7a822ac..466ebe3 100644 --- a/src/main/runtime/overlay-visibility-runtime-main-deps.ts +++ b/src/main/runtime/overlay-visibility-runtime-main-deps.ts @@ -2,29 +2,19 @@ import type { BrowserWindow } from 'electron'; import type { WindowGeometry } from '../../types'; import type { OverlayVisibilityRuntimeDeps } from '../overlay-visibility-runtime'; -export function createBuildOverlayVisibilityRuntimeMainDepsHandler(deps: { - getMainWindow: () => BrowserWindow | null; - 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; -}) { +export function createBuildOverlayVisibilityRuntimeMainDepsHandler( + deps: OverlayVisibilityRuntimeDeps, +) { return (): OverlayVisibilityRuntimeDeps => ({ getMainWindow: () => deps.getMainWindow(), getInvisibleWindow: () => deps.getInvisibleWindow(), getVisibleOverlayVisible: () => deps.getVisibleOverlayVisible(), getInvisibleOverlayVisible: () => deps.getInvisibleOverlayVisible(), - getWindowTracker: () => deps.getWindowTracker() as never, + getWindowTracker: () => deps.getWindowTracker(), getTrackerNotReadyWarningShown: () => deps.getTrackerNotReadyWarningShown(), setTrackerNotReadyWarningShown: (shown: boolean) => deps.setTrackerNotReadyWarningShown(shown), - updateVisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateVisibleOverlayBounds(geometry), + updateVisibleOverlayBounds: (geometry: WindowGeometry) => + deps.updateVisibleOverlayBounds(geometry), updateInvisibleOverlayBounds: (geometry: WindowGeometry) => deps.updateInvisibleOverlayBounds(geometry), ensureOverlayWindowLevel: (window: BrowserWindow) => deps.ensureOverlayWindowLevel(window), diff --git a/src/main/runtime/startup-config-main-deps.test.ts b/src/main/runtime/startup-config-main-deps.test.ts index 2aebf08..229ff93 100644 --- a/src/main/runtime/startup-config-main-deps.test.ts +++ b/src/main/runtime/startup-config-main-deps.test.ts @@ -8,7 +8,7 @@ import { test('reload config main deps builder maps callbacks and fail handlers', async () => { const calls: string[] = []; const deps = createBuildReloadConfigMainDepsHandler({ - reloadConfigStrict: () => ({ ok: true }), + reloadConfigStrict: () => ({ ok: true, path: '/tmp/config.jsonc', warnings: [] }), logInfo: (message) => calls.push(`info:${message}`), logWarning: (message) => calls.push(`warn:${message}`), 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.logWarning('y'); deps.showDesktopNotification('SubMiner', { body: 'warn' }); diff --git a/src/main/runtime/startup-config-main-deps.ts b/src/main/runtime/startup-config-main-deps.ts index 989a601..9670549 100644 --- a/src/main/runtime/startup-config-main-deps.ts +++ b/src/main/runtime/startup-config-main-deps.ts @@ -1,18 +1,11 @@ -export function createBuildReloadConfigMainDepsHandler(deps: { - reloadConfigStrict: () => unknown; - logInfo: (message: string) => void; - logWarning: (message: string) => void; - showDesktopNotification: (title: string, options: { body: string }) => void; - startConfigHotReload: () => void; - refreshAnilistClientSecretState: (options: { force: boolean }) => Promise; - failHandlers: { - logError: (details: string) => void; - showErrorBox: (title: string, details: string) => void; - quit: () => void; - }; -}) { - return () => ({ - reloadConfigStrict: () => deps.reloadConfigStrict() as never, +import type { createCriticalConfigErrorHandler, createReloadConfigHandler } from './startup-config'; + +type ReloadConfigMainDeps = Parameters[0]; +type CriticalConfigErrorMainDeps = Parameters[0]; + +export function createBuildReloadConfigMainDepsHandler(deps: ReloadConfigMainDeps) { + return (): ReloadConfigMainDeps => ({ + reloadConfigStrict: () => deps.reloadConfigStrict(), logInfo: (message: string) => deps.logInfo(message), logWarning: (message: string) => deps.logWarning(message), showDesktopNotification: (title: string, options: { body: string }) => @@ -22,25 +15,20 @@ export function createBuildReloadConfigMainDepsHandler(deps: { deps.refreshAnilistClientSecretState(options), failHandlers: { 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(), }, }); } -export function createBuildCriticalConfigErrorMainDepsHandler(deps: { - getConfigPath: () => string; - failHandlers: { - logError: (details: string) => void; - showErrorBox: (title: string, details: string) => void; - quit: () => void; - }; -}) { - return () => ({ +export function createBuildCriticalConfigErrorMainDepsHandler(deps: CriticalConfigErrorMainDeps) { + return (): CriticalConfigErrorMainDeps => ({ getConfigPath: () => deps.getConfigPath(), failHandlers: { 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(), }, }); diff --git a/src/main/runtime/subtitle-tokenization-main-deps.test.ts b/src/main/runtime/subtitle-tokenization-main-deps.test.ts index 17c733b..fc82152 100644 --- a/src/main/runtime/subtitle-tokenization-main-deps.test.ts +++ b/src/main/runtime/subtitle-tokenization-main-deps.test.ts @@ -9,8 +9,8 @@ import { test('tokenizer deps builder records known-word lookups and maps readers', () => { const calls: string[] = []; const deps = createBuildTokenizerDepsMainHandler({ - getYomitanExt: () => ({ id: 'ext' }), - getYomitanParserWindow: () => ({ id: 'window' }), + getYomitanExt: () => null, + getYomitanParserWindow: () => null, setYomitanParserWindow: () => calls.push('set-window'), getYomitanParserReadyPromise: () => null, 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'), isKnownWord: (text) => text === 'known', recordLookup: (hit) => calls.push(`lookup:${hit}`), - getKnownWordMatchMode: () => 'exact', + getKnownWordMatchMode: () => 'surface', getMinSentenceWordsForNPlusOne: () => 3, getJlptLevel: () => 'N2', getJlptEnabled: () => true, getFrequencyDictionaryEnabled: () => true, getFrequencyRank: () => 5, getYomitanGroupDebugEnabled: () => false, - getMecabTokenizer: () => ({ id: 'mecab' }), + getMecabTokenizer: () => null, })(); assert.equal(deps.isKnownWord('known'), true); assert.equal(deps.isKnownWord('unknown'), false); - deps.setYomitanParserWindow({}); + deps.setYomitanParserWindow(null); deps.setYomitanParserReadyPromise(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']); }); diff --git a/src/main/runtime/subtitle-tokenization-main-deps.ts b/src/main/runtime/subtitle-tokenization-main-deps.ts index baa1ee7..7370298 100644 --- a/src/main/runtime/subtitle-tokenization-main-deps.ts +++ b/src/main/runtime/subtitle-tokenization-main-deps.ts @@ -1,30 +1,29 @@ -export function createBuildTokenizerDepsMainHandler(deps: { - getYomitanExt: () => unknown; - getYomitanParserWindow: () => unknown; - setYomitanParserWindow: (window: unknown) => void; - getYomitanParserReadyPromise: () => Promise | null; - setYomitanParserReadyPromise: (promise: Promise | null) => void; - getYomitanParserInitPromise: () => Promise | null; - setYomitanParserInitPromise: (promise: Promise | null) => void; - isKnownWord: (text: string) => boolean; +import type { TokenizerDepsRuntimeOptions } from '../../core/services/tokenizer'; + +type TokenizerMainDeps = TokenizerDepsRuntimeOptions & { + getJlptEnabled: NonNullable; + getFrequencyDictionaryEnabled: NonNullable< + TokenizerDepsRuntimeOptions['getFrequencyDictionaryEnabled'] + >; + getFrequencyRank: NonNullable; + getMinSentenceWordsForNPlusOne: NonNullable< + TokenizerDepsRuntimeOptions['getMinSentenceWordsForNPlusOne'] + >; + getYomitanGroupDebugEnabled: NonNullable< + TokenizerDepsRuntimeOptions['getYomitanGroupDebugEnabled'] + >; recordLookup: (hit: boolean) => void; - getKnownWordMatchMode: () => unknown; - getMinSentenceWordsForNPlusOne: () => number; - getJlptLevel: (text: string) => unknown; - getJlptEnabled: () => boolean; - getFrequencyDictionaryEnabled: () => boolean; - getFrequencyRank: (text: string) => unknown; - getYomitanGroupDebugEnabled: () => boolean; - getMecabTokenizer: () => unknown; -}) { - return () => ({ - getYomitanExt: () => deps.getYomitanExt() as never, - getYomitanParserWindow: () => deps.getYomitanParserWindow() as never, - setYomitanParserWindow: (window: unknown) => deps.setYomitanParserWindow(window), - getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise() as never, +}; + +export function createBuildTokenizerDepsMainHandler(deps: TokenizerMainDeps) { + return (): TokenizerDepsRuntimeOptions => ({ + getYomitanExt: () => deps.getYomitanExt(), + getYomitanParserWindow: () => deps.getYomitanParserWindow(), + setYomitanParserWindow: (window) => deps.setYomitanParserWindow(window), + getYomitanParserReadyPromise: () => deps.getYomitanParserReadyPromise(), setYomitanParserReadyPromise: (promise: Promise | null) => deps.setYomitanParserReadyPromise(promise), - getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise() as never, + getYomitanParserInitPromise: () => deps.getYomitanParserInitPromise(), setYomitanParserInitPromise: (promise: Promise | null) => deps.setYomitanParserInitPromise(promise), isKnownWord: (text: string) => { @@ -32,14 +31,14 @@ export function createBuildTokenizerDepsMainHandler(deps: { deps.recordLookup(hit); return hit; }, - getKnownWordMatchMode: () => deps.getKnownWordMatchMode() as never, + getKnownWordMatchMode: () => deps.getKnownWordMatchMode(), getMinSentenceWordsForNPlusOne: () => deps.getMinSentenceWordsForNPlusOne(), - getJlptLevel: (text: string) => deps.getJlptLevel(text) as never, + getJlptLevel: (text: string) => deps.getJlptLevel(text), getJlptEnabled: () => deps.getJlptEnabled(), getFrequencyDictionaryEnabled: () => deps.getFrequencyDictionaryEnabled(), - getFrequencyRank: (text: string) => deps.getFrequencyRank(text) as never, + getFrequencyRank: (text: string) => deps.getFrequencyRank(text), getYomitanGroupDebugEnabled: () => deps.getYomitanGroupDebugEnabled(), - getMecabTokenizer: () => deps.getMecabTokenizer() as never, + getMecabTokenizer: () => deps.getMecabTokenizer(), }); } diff --git a/src/main/runtime/tray-runtime-handlers.ts b/src/main/runtime/tray-runtime-handlers.ts index 622da67..92f60c1 100644 --- a/src/main/runtime/tray-runtime-handlers.ts +++ b/src/main/runtime/tray-runtime-handlers.ts @@ -1,22 +1,34 @@ import { createDestroyTrayHandler, createEnsureTrayHandler } from './tray-lifecycle'; -import { createBuildDestroyTrayMainDepsHandler, createBuildEnsureTrayMainDepsHandler } from './app-runtime-main-deps'; -import { createBuildTrayMenuTemplateHandler, createResolveTrayIconPathHandler } from './tray-main-actions'; +import { + createBuildDestroyTrayMainDepsHandler, + createBuildEnsureTrayMainDepsHandler, +} from './app-runtime-main-deps'; +import { + createBuildTrayMenuTemplateHandler, + createResolveTrayIconPathHandler, +} from './tray-main-actions'; import { createBuildResolveTrayIconPathMainDepsHandler, createBuildTrayMenuTemplateMainDepsHandler, } from './tray-main-deps'; -type ResolveTrayIconPathMainDeps = Parameters[0]; +type ResolveTrayIconPathMainDeps = Parameters< + typeof createBuildResolveTrayIconPathMainDepsHandler +>[0]; type BuildTrayMenuTemplateMainDeps = Parameters< typeof createBuildTrayMenuTemplateMainDepsHandler >[0]; -type EnsureTrayMainDeps = Parameters[0]; -type DestroyTrayMainDeps = Parameters[0]; +type EnsureTrayMainDeps = Parameters< + typeof createBuildEnsureTrayMainDepsHandler +>[0]; +type TrayLike = NonNullable[0]['getTray']>>; +type TrayIconLike = Parameters[0]['createTray']>[0]; +type DestroyTrayMainDeps = Parameters>[0]; export function createTrayRuntimeHandlers(deps: { resolveTrayIconPathDeps: ResolveTrayIconPathMainDeps; buildTrayMenuTemplateDeps: BuildTrayMenuTemplateMainDeps; - ensureTrayDeps: Omit; + ensureTrayDeps: Omit, 'buildTrayMenu' | 'resolveTrayIconPath'>; destroyTrayDeps: DestroyTrayMainDeps; buildMenuFromTemplate: (template: TMenuItem[]) => TMenu; }) {