fix(plugin): allow cold-start overlay launch without running process

This commit is contained in:
2026-02-22 21:08:25 -08:00
parent f33b5e1e98
commit a07d5ecdb3
13 changed files with 795 additions and 132 deletions

View File

@@ -0,0 +1,33 @@
---
id: TASK-116
title: Analyze git history and draft initial release cleanup plan
status: Done
assignee: []
created_date: '2026-02-23 04:40'
updated_date: '2026-02-23 04:44'
labels:
- release
- git
- planning
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Review current main branch commit history and project structure, recommend best pre-release history cleanup strategy, and produce a copy/paste command plan in initial-release.md.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 Current commit history and repo state are analyzed
- [x] #2 Recommended history-cleanup strategy is justified
- [x] #3 initial-release.md contains exact copy/paste commands with safety checks and team cutover steps
<!-- AC:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Analyzed `main` history and repository state, then recommended an orphan-branch history rewrite with curated commits as the safest pre-release cleanup path. Added `initial-release.md` with exact copy/paste commands for backups, optional stash handling, orphan branch creation, 7-commit split staging strategy, tree-equivalence validation, force-with-lease cutover, tagging, and collaborator resync instructions.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -0,0 +1,60 @@
---
id: TASK-117
title: Fix mpv plugin overlay start gate when SubMiner is not running
status: Done
assignee:
- codex
created_date: '2026-02-23 04:49'
updated_date: '2026-02-23 04:50'
labels:
- bug
- plugin
- regression
dependencies: []
priority: high
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Recent `plugin/subminer.lua` change added an IPC readiness gate in `start_overlay()` that rejects start when no SubMiner process exists yet. This blocks manual start (`y-s`) and script-message start even when binary path overrides are valid (`SUBMINER_APPIMAGE_PATH`, `SUBMINER_BINARY_PATH`, `binary_path`).
Need to restore expected behavior:
- allow cold start when process is not running,
- preserve mismatch/misconfiguration checks when process is already running,
- add regression coverage for the new start gate behavior.
<!-- SECTION:DESCRIPTION:END -->
## Action Steps
<!-- SECTION:PLAN:BEGIN -->
1. Add failing regression test covering cold-start path from plugin start gate.
2. Patch `start_overlay()` gate logic so process-not-running does not block initial start.
3. Keep existing IPC mismatch/missing-socket checks for running-process path.
4. Re-run plugin validation and focused tests.
<!-- SECTION:PLAN:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [x] #1 `y-s` / `subminer-start` can launch overlay when SubMiner is not yet running and binary is available.
- [x] #2 Plugin still surfaces clear refusal for true IPC/socket mismatches after process is running.
- [x] #3 Regression test exists and fails before fix / passes after fix.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
- Patched `plugin/subminer.lua` start gate: `start_overlay()` now treats `SubMiner process not running` as a valid cold-start condition and only refuses start for other IPC readiness failures.
- Added regression harness `scripts/test-plugin-start-gate.lua` to load plugin with mocked mpv APIs and assert `subminer-start` triggers `--start` command for cold-start path.
- Validation:
- red: `SUBMINER_APPIMAGE_PATH=/tmp/subminer-binary lua scripts/test-plugin-start-gate.lua` (failed before patch with expected cold-start assertion),
- green: same command passes after patch,
- `luac -p plugin/subminer.lua`,
- `bun test launcher/mpv.test.ts`.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
Fixed mpv plugin start regression by allowing cold-start launch when SubMiner process is not yet running, while preserving existing refusal path for non-cold-start IPC readiness errors. Added an executable Lua regression harness for this path and validated with syntax + focused launcher MPV tests.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -5,6 +5,7 @@
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed. * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/ */
{ {
// ========================================== // ==========================================
// Overlay Auto-Start // Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles. // When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
@@ -23,7 +24,7 @@
// Control whether browser opens automatically for texthooker. // Control whether browser opens automatically for texthooker.
// ========================================== // ==========================================
"texthooker": { "texthooker": {
"openBrowser": true, // Open browser setting. Values: true | false "openBrowser": true // Open browser setting. Values: true | false
}, // Control whether browser opens automatically for texthooker. }, // Control whether browser opens automatically for texthooker.
// ========================================== // ==========================================
@@ -33,7 +34,7 @@
// ========================================== // ==========================================
"websocket": { "websocket": {
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false "enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
"port": 6677, // Built-in subtitle websocket server port. "port": 6677 // Built-in subtitle websocket server port.
}, // Built-in WebSocket server broadcasts subtitle text to connected clients. }, // Built-in WebSocket server broadcasts subtitle text to connected clients.
// ========================================== // ==========================================
@@ -42,7 +43,7 @@
// Set to debug for full runtime diagnostics. // Set to debug for full runtime diagnostics.
// ========================================== // ==========================================
"logging": { "logging": {
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error "level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
}, // Controls logging verbosity. }, // Controls logging verbosity.
// ========================================== // ==========================================
@@ -64,7 +65,7 @@
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting. "toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting. "markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting. "openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting. "openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ========================================== // ==========================================
@@ -74,7 +75,7 @@
// This edit-mode shortcut is fixed and is not currently configurable. // This edit-mode shortcut is fixed and is not currently configurable.
// ========================================== // ==========================================
"invisibleOverlay": { "invisibleOverlay": {
"startupVisibility": "platform-default", // Startup visibility setting. "startupVisibility": "platform-default" // Startup visibility setting.
}, // Startup behavior for the invisible interactive subtitle mining layer. }, // Startup behavior for the invisible interactive subtitle mining layer.
// ========================================== // ==========================================
@@ -94,7 +95,7 @@
"secondarySub": { "secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting. "secondarySubLanguages": [], // Secondary sub languages setting.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false "autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
"defaultMode": "hover", // Default mode setting. "defaultMode": "hover" // Default mode setting.
}, // Dual subtitle track options. }, // Dual subtitle track options.
// ========================================== // ==========================================
@@ -105,7 +106,7 @@
"defaultMode": "auto", // Subsync default mode. Values: auto | manual "defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Alass path setting. "alass_path": "", // Alass path setting.
"ffsubsync_path": "", // Ffsubsync path setting. "ffsubsync_path": "", // Ffsubsync path setting.
"ffmpeg_path": "", // Ffmpeg path setting. "ffmpeg_path": "" // Ffmpeg path setting.
}, // Subsync engine and executable paths. }, // Subsync engine and executable paths.
// ========================================== // ==========================================
@@ -113,7 +114,7 @@
// Initial vertical subtitle position from the bottom. // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
"subtitlePosition": { "subtitlePosition": {
"yPercent": 10, // Y percent setting. "yPercent": 10 // Y percent setting.
}, // Initial vertical subtitle position from the bottom. }, // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
@@ -138,7 +139,7 @@
"N2": "#f5a97f", // N2 setting. "N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting. "N3": "#f9e2af", // N3 setting.
"N4": "#a6e3a1", // N4 setting. "N4": "#a6e3a1", // N4 setting.
"N5": "#8aadf4", // N5 setting. "N5": "#8aadf4" // N5 setting.
}, // Jlpt colors setting. }, // Jlpt colors setting.
"frequencyDictionary": { "frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false "enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
@@ -146,7 +147,13 @@
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000). "topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded "mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`. "singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX). "bandedColors": [
"#ed8796",
"#f5a97f",
"#f9e2af",
"#a6e3a1",
"#8aadf4"
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting. }, // Frequency dictionary setting.
"secondary": { "secondary": {
"fontSize": 24, // Font size setting. "fontSize": 24, // Font size setting.
@@ -154,8 +161,8 @@
"backgroundColor": "transparent", // Background color setting. "backgroundColor": "transparent", // Background color setting.
"fontWeight": "normal", // Font weight setting. "fontWeight": "normal", // Font weight setting.
"fontStyle": "normal", // Font style setting. "fontStyle": "normal", // Font style setting.
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting. "fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif" // Font family setting.
}, // Secondary setting. } // Secondary setting.
}, // Primary and secondary subtitle styling. }, // Primary and secondary subtitle styling.
// ========================================== // ==========================================
@@ -168,13 +175,15 @@
"enabled": false, // Enable AnkiConnect integration. Values: true | false "enabled": false, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting. "url": "http://127.0.0.1:8765", // Url setting.
"pollingRate": 3000, // Polling interval in milliseconds. "pollingRate": 3000, // Polling interval in milliseconds.
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging. "tags": [
"SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"fields": { "fields": {
"audio": "ExpressionAudio", // Audio setting. "audio": "ExpressionAudio", // Audio setting.
"image": "Picture", // Image setting. "image": "Picture", // Image setting.
"sentence": "Sentence", // Sentence setting. "sentence": "Sentence", // Sentence setting.
"miscInfo": "MiscInfo", // Misc info setting. "miscInfo": "MiscInfo", // Misc info setting.
"translation": "SelectionText", // Translation setting. "translation": "SelectionText" // Translation setting.
}, // Fields setting. }, // Fields setting.
"ai": { "ai": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
@@ -183,7 +192,7 @@
"model": "openai/gpt-4o-mini", // Model setting. "model": "openai/gpt-4o-mini", // Model setting.
"baseUrl": "https://openrouter.ai/api", // Base url setting. "baseUrl": "https://openrouter.ai/api", // Base url setting.
"targetLanguage": "English", // Target language setting. "targetLanguage": "English", // Target language setting.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting. "systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting.
}, // Ai setting. }, // Ai setting.
"media": { "media": {
"generateAudio": true, // Generate audio setting. Values: true | false "generateAudio": true, // Generate audio setting. Values: true | false
@@ -196,7 +205,7 @@
"animatedCrf": 35, // Animated crf setting. "animatedCrf": 35, // Animated crf setting.
"audioPadding": 0.5, // Audio padding setting. "audioPadding": 0.5, // Audio padding setting.
"fallbackDuration": 3, // Fallback duration setting. "fallbackDuration": 3, // Fallback duration setting.
"maxMediaDuration": 30, // Max media duration setting. "maxMediaDuration": 30 // Max media duration setting.
}, // Media setting. }, // Media setting.
"behavior": { "behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false "overwriteAudio": true, // Overwrite audio setting. Values: true | false
@@ -204,7 +213,7 @@
"mediaInsertMode": "append", // Media insert mode setting. "mediaInsertMode": "append", // Media insert mode setting.
"highlightWord": true, // Highlight word setting. Values: true | false "highlightWord": true, // Highlight word setting. Values: true | false
"notificationType": "osd", // Notification type setting. "notificationType": "osd", // Notification type setting.
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false "autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting. }, // Behavior setting.
"nPlusOne": { "nPlusOne": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false "highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
@@ -213,20 +222,20 @@
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names. "decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3). "minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight. "nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
"knownWord": "#a6da95", // Color used for legacy known-word highlights. "knownWord": "#a6da95" // Color used for legacy known-word highlights.
}, // N plus one setting. }, // N plus one setting.
"metadata": { "metadata": {
"pattern": "[SubMiner] %f (%t)", // Pattern setting. "pattern": "[SubMiner] %f (%t)" // Pattern setting.
}, // Metadata setting. }, // Metadata setting.
"isLapis": { "isLapis": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
"sentenceCardModel": "Japanese sentences", // Sentence card model setting. "sentenceCardModel": "Japanese sentences" // Sentence card model setting.
}, // Is lapis setting. }, // Is lapis setting.
"isKiku": { "isKiku": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled "fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false "deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
}, // Is kiku setting. } // Is kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ========================================== // ==========================================
@@ -236,7 +245,7 @@
"jimaku": { "jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting. "apiBaseUrl": "https://jimaku.cc", // Api base url setting.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none "languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10, // Maximum Jimaku search results returned. "maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults. }, // Jimaku API configuration and defaults.
// ========================================== // ==========================================
@@ -247,7 +256,10 @@
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off "mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine. "whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
"whisperModel": "", // Path to whisper model used for fallback transcription. "whisperModel": "", // Path to whisper model used for fallback transcription.
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher. "primarySubLanguages": [
"ja",
"jpn"
] // Comma-separated primary subtitle language priority used by the launcher.
}, // Defaults for subminer YouTube subtitle extraction/transcription mode. }, // Defaults for subminer YouTube subtitle extraction/transcription mode.
// ========================================== // ==========================================
@@ -256,7 +268,7 @@
// ========================================== // ==========================================
"anilist": { "anilist": {
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false "enabled": false, // Enable AniList post-watch progress updates. Values: true | false
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup. "accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
}, // Anilist API credentials and update behavior. }, // Anilist API credentials and update behavior.
// ========================================== // ==========================================
@@ -280,8 +292,16 @@
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false "pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons. "iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false "directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions. "directPlayContainers": [
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable. "mkv",
"mp4",
"webm",
"mov",
"flac",
"mp3",
"aac"
], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
}, // Optional Jellyfin integration for auth, browsing, and playback launch. }, // Optional Jellyfin integration for auth, browsing, and playback launch.
// ========================================== // ==========================================
@@ -292,7 +312,7 @@
"discordPresence": { "discordPresence": {
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false "enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
"updateIntervalMs": 3000, // Minimum interval between presence payload updates. "updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates. "debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session. }, // Optional Discord Rich Presence activity card updates for current playback/study session.
// ========================================== // ==========================================
@@ -314,7 +334,7 @@
"telemetryDays": 30, // Telemetry retention window in days. "telemetryDays": 30, // Telemetry retention window in days.
"dailyRollupsDays": 365, // Daily rollup retention window in days. "dailyRollupsDays": 365, // Daily rollup retention window in days.
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days. "monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs. "vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
}, // Retention setting. } // Retention setting.
}, // Enable/disable immersion tracking. } // Enable/disable immersion tracking.
} }

View File

@@ -125,65 +125,108 @@ src/renderer/
## Flow Diagram ## Flow Diagram
The main process has three layers: `main.ts` delegates to composition modules that wire together domain services. The renderer runs in a separate Electron process, connected through `preload.ts`. The main process has three layers: `main.ts` delegates to composition modules that wire together domain services. Three overlay windows (visible, invisible, secondary) run in separate Electron renderer processes, connected through `preload.ts`. External runtimes (launcher CLI and mpv plugin) operate independently and communicate via IPC socket or CLI passthrough.
```mermaid ```mermaid
flowchart TD flowchart TD
classDef entry fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
classDef comp fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef comp fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef svc fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef bridge fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef rend fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef rend fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef ext fill:#a6da95,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef ext fill:#a6da95,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef extrt fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
Main["main.ts"]:::entry Main["main.ts — composition root"]:::entry
subgraph Comp["Composition — src/main/"] subgraph Comp["Composition — src/main/"]
Startup["Startup & Lifecycle<br/>startup · app-lifecycle<br/>startup-lifecycle · state"]:::comp direction TB
Wiring["Runtime Wiring<br/>ipc-runtime · cli-runtime<br/>overlay-runtime · subsync-runtime"]:::comp Startup["Startup & Lifecycle<br/>startup · app-lifecycle · startup-lifecycle · state"]:::comp
Wiring["Runtime Wiring<br/>ipc-runtime · cli-runtime · overlay-runtime · subsync-runtime"]:::comp
Composers["Composers<br/>mpv-runtime · anilist-tracking · jellyfin-runtime"]:::comp
end end
subgraph Svc["Services — src/core/services/"] subgraph Svc["Services — src/core/services/"]
direction LR direction TB
Mpv["MPV Stack<br/>transport · protocol<br/>state · properties"]:::svc subgraph SvcRow1[" "]
Overlay["Overlay<br/>manager · window<br/>visibility · bridge"]:::svc direction LR
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc Mpv["MPV Stack<br/>transport · protocol<br/>properties · render-metrics"]:::svc
Integrations["Integrations<br/>jimaku · subsync<br/>texthooker · yomitan"]:::svc Overlay["Overlay Manager<br/>window · geometry<br/>visibility · bridge"]:::svc
end
subgraph SvcRow2[" "]
direction LR
Mining["Mining & Subtitles<br/>mining · field-grouping<br/>subtitle-ws · tokenizer"]:::svc
Integrations["Integrations<br/>jimaku · subsync · texthooker<br/>yomitan · discord-presence"]:::svc
end
subgraph SvcRow3[" "]
direction LR
Tracking["Tracking<br/>anilist · jellyfin-remote<br/>immersion-tracker"]:::svc
Config["Config & Runtime<br/>config-hot-reload<br/>runtime-options"]:::svc
end
end end
Bridge(["preload.ts — Electron IPC"]):::bridge Bridge(["preload.ts — Electron IPC bridge"]):::bridge
subgraph Rend["Renderer — src/renderer/"] subgraph Rend["Renderer — src/renderer/"]
Orchestration["renderer.ts<br/>orchestration · IPC wiring"]:::rend direction TB
UI["subtitle-render · positioning<br/>handlers · modals"]:::rend subgraph Windows["Three overlay windows"]
direction LR
Visible["Visible<br/>interactive Yomitan lookups"]:::rend
Invisible["Invisible<br/>mpv-matched positioning"]:::rend
Secondary["Secondary<br/>secondary subtitle bar"]:::rend
end
UI["subtitle-render · positioning · handlers · modals"]:::rend
end end
subgraph Ext["External Systems"] subgraph Ext["External Systems"]
direction LR direction LR
mpv["mpv"]:::ext mpvExt["mpv player"]:::ext
Anki["AnkiConnect"]:::ext AnkiExt["AnkiConnect"]:::ext
Jimaku["Jimaku API"]:::ext JimakuExt["Jimaku API"]:::ext
Tracker["Window Tracker"]:::ext TrackerExt["Window Tracker<br/>Hyprland · Sway · X11 · macOS"]:::ext
AnilistExt["AniList API"]:::ext
JellyfinExt["Jellyfin"]:::ext
DiscordExt["Discord RPC"]:::ext
end end
Main -->|delegates| Comp subgraph ExtRt["External Runtimes"]
Startup -->|initializes| Svc direction LR
Wiring -->|dispatches to| Svc Launcher["launcher/<br/>CLI command dispatch"]:::extrt
Plugin["subminer.lua<br/>mpv plugin"]:::extrt
end
Overlay <--> Bridge Main -->|"delegates"| Comp
Startup -->|"initializes"| Svc
Wiring -->|"dispatches to"| Svc
Composers -->|"wires"| Svc
Overlay <-->Bridge
Mining <--> Bridge Mining <--> Bridge
Bridge <--> Orchestration Bridge <--> Visible
Orchestration --> UI Bridge <--> Invisible
Bridge <--> Secondary
Windows --> UI
Mpv <-->|JSON socket| mpv Mpv <-->|"JSON IPC socket"| mpvExt
Mining -->|HTTP| Anki Mining -->|"HTTP"| AnkiExt
Integrations -->|HTTP| Jimaku Integrations -->|"HTTP"| JimakuExt
Overlay --> Tracker Overlay -->|"platform API"| TrackerExt
Tracking -->|"HTTP"| AnilistExt
Tracking -->|"HTTP"| JellyfinExt
Integrations -->|"RPC"| DiscordExt
Launcher -->|"CLI passthrough"| Main
Plugin -->|"IPC socket"| mpvExt
style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5 style Comp fill:#363a4f,stroke:#494d64,color:#cad3f5
style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5 style Svc fill:#363a4f,stroke:#494d64,color:#cad3f5
style SvcRow1 fill:transparent,stroke:none
style SvcRow2 fill:transparent,stroke:none
style SvcRow3 fill:transparent,stroke:none
style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5 style Rend fill:#363a4f,stroke:#494d64,color:#cad3f5
style Windows fill:#1e2030,stroke:#494d64,color:#cad3f5
style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5 style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5
style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
``` ```
## Composition Pattern ## Composition Pattern
@@ -242,52 +285,81 @@ For domains migrated to reducer-style transitions (for example AniList token/que
## Program Lifecycle ## Program Lifecycle
- **Startup:** `startup.ts` parses CLI args and detects the compositor backend. If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks. - **Module-level init:** Before `app.ready`, the composition root registers protocols, sets platform flags, constructs all services, and wires dependency injection. `runAndApplyStartupState()` parses CLI args and detects the compositor backend.
- **Initialization:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs the critical path first (strict config reload, runtime options + keybindings, mpv client creation, overlay/IPC setup). Non-critical warmups are launched asynchronously (`mecab`, `yomitan-extension`, dictionary prewarm, optional Jellyfin remote session). - **Startup:** If `--generate-config` is passed, it writes the template and exits. Otherwise `app-lifecycle.ts` acquires the single-instance lock and registers Electron lifecycle hooks.
- **Runtime:** Event-driven. mpv events, IPC messages, CLI commands, overlay shortcuts, hot-reload notifications, and integration callbacks route through runtime handlers/composers, update `AppState`, and broadcast to overlay windows. - **Critical-path init:** Once `app.whenReady()` fires, `composeAppReadyRuntime()` runs strict config reload, resolves keybindings, creates the `MpvIpcClient` (which immediately connects and subscribes to 26 properties), and initializes the `RuntimeOptionsManager`, `SubtitleTimingTracker`, and `ImmersionTrackerService`.
- **Overlay window model:** runtime manages three overlay windows: `visible`, `invisible`, and `secondary`. `splitOverlayGeometryForSecondaryBar()` reserves the top 20% for the secondary subtitle bar and routes the remaining area to the active primary overlay layer. - **Overlay runtime:** `initializeOverlayRuntime()` creates three overlay windows**visible** (interactive Yomitan lookups), **invisible** (mpv-matched subtitle positioning), and **secondary** (secondary subtitle bar, top 20% via `splitOverlayGeometryForSecondaryBar`) — then registers global shortcuts and sets initial bounds from the window tracker.
- **Shutdown:** `onWillQuitCleanup` tears down tray + watchers + integrations, stops subtitle/texthooker servers, flushes buffered MPV OSD log writes, closes token/session windows, and stops Jellyfin/Discord runtime services. - **Background warmups:** Non-critical services are launched asynchronously: MeCab tokenizer check, Yomitan extension load, JLPT + frequency dictionary prewarm, optional Jellyfin remote session, Discord presence service, and AniList token refresh.
- **Runtime:** Event-driven. mpv property changes, IPC messages, CLI commands, overlay shortcuts, and hot-reload notifications route through runtime handlers/composers. Subtitle text flows through `SubtitlePipeline` (normalize → tokenize → merge), and results broadcast to all overlay windows.
- **Shutdown:** `onWillQuitCleanup` destroys tray + config watcher, unregisters shortcuts, stops WebSocket + texthooker servers, closes the mpv socket + flushes OSD log, stops the window tracker, closes the Yomitan parser window, flushes the immersion tracker (SQLite), stops Jellyfin/Discord services, and cleans Anki/AniList state.
```mermaid ```mermaid
flowchart TD flowchart LR
classDef start fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px classDef start fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
classDef phase fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef decision fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef init fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef runtime fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef runtime fill:#8bd5ca,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef shutdown fill:#ed8796,stroke:#363a4f,color:#24273a,stroke-width:1.5px classDef shutdown fill:#ed8796,stroke:#494d64,color:#24273a,stroke-width:1.5px
classDef warmup fill:#eed49f,stroke:#494d64,color:#24273a,stroke-width:1.5px
CLI["CLI args & environment"]:::start CLI["CLI args &<br/>environment"]:::start
CLI --> Parse["startup.ts<br/>Parse argv · detect backend · resolve config"]:::phase CLI --> Proto["Module-level init<br/>register protocols<br/>construct services<br/>wire deps"]:::phase
Parse --> GenCheck{"--generate-config?"}:::decision Proto --> Parse["startup.ts<br/>parse argv<br/>detect backend"]:::phase
GenCheck -->|yes| GenExit["Write config template & exit"]:::phase Parse --> GenCheck{"--generate<br/>-config?"}:::decision
GenCheck -->|no| Lifecycle["app-lifecycle.ts<br/>Acquire single-instance lock<br/>Register Electron lifecycle hooks"]:::phase GenCheck -->|"yes"| GenExit["Write template<br/>& exit"]:::phase
Lifecycle -->|"app.whenReady()"| Ready["startup-lifecycle.ts"]:::phase GenCheck -->|"no"| Lock["app-lifecycle.ts<br/>single-instance lock<br/>lifecycle hooks"]:::phase
Ready --> Init Lock -->|"app.whenReady()"| Ready["composeAppReady<br/>Runtime()"]:::phase
subgraph Init["Initialization"]
direction LR
Config["Load config<br/>resolve keybindings"]:::init
Runtime["Create mpv client<br/>init runtime options"]:::init
Platform["Start window tracker<br/>WebSocket policy"]:::init
end
Init --> Create["Create overlay window<br/>Establish IPC bridge"]:::phase Ready --> Config["Config reload<br/>keybindings<br/>log level"]:::init
Create --> Warm["Background warmups<br/>MeCab · Yomitan · dictionaries · Jellyfin"]:::phase Ready --> MpvInit["MpvIpcClient<br/>connect socket<br/>subscribe 26 props"]:::init
Ready --> Platform["RuntimeOptions<br/>timing tracker<br/>immersion tracker"]:::init
Config --> OverlayInit
MpvInit --> OverlayInit
Platform --> OverlayInit
OverlayInit["initializeOverlay<br/>Runtime()"]:::phase
OverlayInit --> VisWin["Visible window<br/>Yomitan lookups"]:::init
OverlayInit --> InvWin["Invisible window<br/>mpv positioning"]:::init
OverlayInit --> SecWin["Secondary window<br/>subtitle bar"]:::init
OverlayInit --> Shortcuts["Register global<br/>shortcuts"]:::init
VisWin --> Warmups
InvWin --> Warmups
SecWin --> Warmups
Shortcuts --> Warmups
Warmups["Background<br/>warmups"]:::phase
Warmups --> W1["MeCab"]:::warmup
Warmups --> W2["Yomitan"]:::warmup
Warmups --> W3["JLPT + freq<br/>dictionaries"]:::warmup
Warmups --> W4["Jellyfin"]:::warmup
Warmups --> W5["Discord"]:::warmup
Warmups --> W6["AniList"]:::warmup
W1 & W2 & W3 & W4 & W5 & W6 --> Loop
Warm --> Loop
subgraph Loop["Runtime — event-driven"] subgraph Loop["Runtime — event-driven"]
direction LR direction TB
Events["mpv · IPC · CLI<br/>shortcut events"]:::runtime MpvEvt["mpv events: subtitle · timing · metrics"]:::runtime
Dispatch["Route to service<br/>via composition layer"]:::runtime IpcEvt["IPC: renderer requests · CLI commands"]:::runtime
State["Update state<br/>broadcast to renderer"]:::runtime ExtEvt["Shortcuts · config hot-reload"]:::runtime
Events --> Dispatch --> State MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::runtime
Route --> Process["SubtitlePipeline<br/>normalize → tokenize → merge"]:::runtime
Process --> Broadcast["Update AppState<br/>broadcast to windows"]:::runtime
end end
Loop -->|"app close"| Quit["Electron will-quit"]:::shutdown Loop -->|"quit signal"| Quit["will-quit"]:::shutdown
Quit --> Teardown["Close mpv socket · unregister shortcuts<br/>Stop WebSocket & texthooker<br/>Destroy tracker · clean Anki state"]:::shutdown
Quit --> T1["Tray · config watcher<br/>global shortcuts"]:::shutdown
Quit --> T2["WebSocket · texthooker<br/>mpv socket · OSD log"]:::shutdown
Quit --> T3["Window tracker<br/>Yomitan parser"]:::shutdown
Quit --> T4["Immersion tracker<br/>Jellyfin · Discord<br/>Anki · AniList"]:::shutdown
style Init fill:#363a4f,stroke:#494d64,color:#cad3f5
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5 style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
``` ```

View File

@@ -5,6 +5,7 @@
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed. * Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/ */
{ {
// ========================================== // ==========================================
// Overlay Auto-Start // Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles. // When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
@@ -23,7 +24,7 @@
// Control whether browser opens automatically for texthooker. // Control whether browser opens automatically for texthooker.
// ========================================== // ==========================================
"texthooker": { "texthooker": {
"openBrowser": true, // Open browser setting. Values: true | false "openBrowser": true // Open browser setting. Values: true | false
}, // Control whether browser opens automatically for texthooker. }, // Control whether browser opens automatically for texthooker.
// ========================================== // ==========================================
@@ -33,7 +34,7 @@
// ========================================== // ==========================================
"websocket": { "websocket": {
"enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false "enabled": "auto", // Built-in subtitle websocket server mode. Values: auto | true | false
"port": 6677, // Built-in subtitle websocket server port. "port": 6677 // Built-in subtitle websocket server port.
}, // Built-in WebSocket server broadcasts subtitle text to connected clients. }, // Built-in WebSocket server broadcasts subtitle text to connected clients.
// ========================================== // ==========================================
@@ -42,7 +43,7 @@
// Set to debug for full runtime diagnostics. // Set to debug for full runtime diagnostics.
// ========================================== // ==========================================
"logging": { "logging": {
"level": "info", // Minimum log level for runtime logging. Values: debug | info | warn | error "level": "info" // Minimum log level for runtime logging. Values: debug | info | warn | error
}, // Controls logging verbosity. }, // Controls logging verbosity.
// ========================================== // ==========================================
@@ -64,7 +65,7 @@
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting. "toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting. "markAudioCard": "CommandOrControl+Shift+A", // Mark audio card setting.
"openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting. "openRuntimeOptions": "CommandOrControl+Shift+O", // Open runtime options setting.
"openJimaku": "Ctrl+Shift+J", // Open jimaku setting. "openJimaku": "Ctrl+Shift+J" // Open jimaku setting.
}, // Overlay keyboard shortcuts. Set a shortcut to null to disable. }, // Overlay keyboard shortcuts. Set a shortcut to null to disable.
// ========================================== // ==========================================
@@ -74,7 +75,7 @@
// This edit-mode shortcut is fixed and is not currently configurable. // This edit-mode shortcut is fixed and is not currently configurable.
// ========================================== // ==========================================
"invisibleOverlay": { "invisibleOverlay": {
"startupVisibility": "platform-default", // Startup visibility setting. "startupVisibility": "platform-default" // Startup visibility setting.
}, // Startup behavior for the invisible interactive subtitle mining layer. }, // Startup behavior for the invisible interactive subtitle mining layer.
// ========================================== // ==========================================
@@ -94,7 +95,7 @@
"secondarySub": { "secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting. "secondarySubLanguages": [], // Secondary sub languages setting.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false "autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
"defaultMode": "hover", // Default mode setting. "defaultMode": "hover" // Default mode setting.
}, // Dual subtitle track options. }, // Dual subtitle track options.
// ========================================== // ==========================================
@@ -105,7 +106,7 @@
"defaultMode": "auto", // Subsync default mode. Values: auto | manual "defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Alass path setting. "alass_path": "", // Alass path setting.
"ffsubsync_path": "", // Ffsubsync path setting. "ffsubsync_path": "", // Ffsubsync path setting.
"ffmpeg_path": "", // Ffmpeg path setting. "ffmpeg_path": "" // Ffmpeg path setting.
}, // Subsync engine and executable paths. }, // Subsync engine and executable paths.
// ========================================== // ==========================================
@@ -113,7 +114,7 @@
// Initial vertical subtitle position from the bottom. // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
"subtitlePosition": { "subtitlePosition": {
"yPercent": 10, // Y percent setting. "yPercent": 10 // Y percent setting.
}, // Initial vertical subtitle position from the bottom. }, // Initial vertical subtitle position from the bottom.
// ========================================== // ==========================================
@@ -138,7 +139,7 @@
"N2": "#f5a97f", // N2 setting. "N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting. "N3": "#f9e2af", // N3 setting.
"N4": "#a6e3a1", // N4 setting. "N4": "#a6e3a1", // N4 setting.
"N5": "#8aadf4", // N5 setting. "N5": "#8aadf4" // N5 setting.
}, // Jlpt colors setting. }, // Jlpt colors setting.
"frequencyDictionary": { "frequencyDictionary": {
"enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false "enabled": false, // Enable frequency-dictionary-based highlighting based on token rank. Values: true | false
@@ -146,7 +147,13 @@
"topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000). "topX": 1000, // Only color tokens with frequency rank <= topX (default: 1000).
"mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded "mode": "single", // single: use one color for all matching tokens. banded: use color ramp by frequency band. Values: single | banded
"singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`. "singleColor": "#f5a97f", // Color used when frequencyDictionary.mode is `single`.
"bandedColors": ["#ed8796", "#f5a97f", "#f9e2af", "#a6e3a1", "#8aadf4"], // Five colors used for rank bands when mode is `banded` (from most common to least within topX). "bandedColors": [
"#ed8796",
"#f5a97f",
"#f9e2af",
"#a6e3a1",
"#8aadf4"
] // Five colors used for rank bands when mode is `banded` (from most common to least within topX).
}, // Frequency dictionary setting. }, // Frequency dictionary setting.
"secondary": { "secondary": {
"fontSize": 24, // Font size setting. "fontSize": 24, // Font size setting.
@@ -154,8 +161,8 @@
"backgroundColor": "transparent", // Background color setting. "backgroundColor": "transparent", // Background color setting.
"fontWeight": "normal", // Font weight setting. "fontWeight": "normal", // Font weight setting.
"fontStyle": "normal", // Font style setting. "fontStyle": "normal", // Font style setting.
"fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif", // Font family setting. "fontFamily": "M PLUS 1, Noto Sans CJK JP Regular, Noto Sans CJK JP, Hiragino Sans, Hiragino Kaku Gothic ProN, Yu Gothic, Arial Unicode MS, Arial, sans-serif" // Font family setting.
}, // Secondary setting. } // Secondary setting.
}, // Primary and secondary subtitle styling. }, // Primary and secondary subtitle styling.
// ========================================== // ==========================================
@@ -168,13 +175,15 @@
"enabled": false, // Enable AnkiConnect integration. Values: true | false "enabled": false, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting. "url": "http://127.0.0.1:8765", // Url setting.
"pollingRate": 3000, // Polling interval in milliseconds. "pollingRate": 3000, // Polling interval in milliseconds.
"tags": ["SubMiner"], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging. "tags": [
"SubMiner"
], // Tags to add to cards mined or updated by SubMiner. Provide an empty array to disable automatic tagging.
"fields": { "fields": {
"audio": "ExpressionAudio", // Audio setting. "audio": "ExpressionAudio", // Audio setting.
"image": "Picture", // Image setting. "image": "Picture", // Image setting.
"sentence": "Sentence", // Sentence setting. "sentence": "Sentence", // Sentence setting.
"miscInfo": "MiscInfo", // Misc info setting. "miscInfo": "MiscInfo", // Misc info setting.
"translation": "SelectionText", // Translation setting. "translation": "SelectionText" // Translation setting.
}, // Fields setting. }, // Fields setting.
"ai": { "ai": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
@@ -183,7 +192,7 @@
"model": "openai/gpt-4o-mini", // Model setting. "model": "openai/gpt-4o-mini", // Model setting.
"baseUrl": "https://openrouter.ai/api", // Base url setting. "baseUrl": "https://openrouter.ai/api", // Base url setting.
"targetLanguage": "English", // Target language setting. "targetLanguage": "English", // Target language setting.
"systemPrompt": "You are a translation engine. Return only the translated text with no explanations.", // System prompt setting. "systemPrompt": "You are a translation engine. Return only the translated text with no explanations." // System prompt setting.
}, // Ai setting. }, // Ai setting.
"media": { "media": {
"generateAudio": true, // Generate audio setting. Values: true | false "generateAudio": true, // Generate audio setting. Values: true | false
@@ -196,7 +205,7 @@
"animatedCrf": 35, // Animated crf setting. "animatedCrf": 35, // Animated crf setting.
"audioPadding": 0.5, // Audio padding setting. "audioPadding": 0.5, // Audio padding setting.
"fallbackDuration": 3, // Fallback duration setting. "fallbackDuration": 3, // Fallback duration setting.
"maxMediaDuration": 30, // Max media duration setting. "maxMediaDuration": 30 // Max media duration setting.
}, // Media setting. }, // Media setting.
"behavior": { "behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false "overwriteAudio": true, // Overwrite audio setting. Values: true | false
@@ -204,7 +213,7 @@
"mediaInsertMode": "append", // Media insert mode setting. "mediaInsertMode": "append", // Media insert mode setting.
"highlightWord": true, // Highlight word setting. Values: true | false "highlightWord": true, // Highlight word setting. Values: true | false
"notificationType": "osd", // Notification type setting. "notificationType": "osd", // Notification type setting.
"autoUpdateNewCards": true, // Automatically update newly added cards. Values: true | false "autoUpdateNewCards": true // Automatically update newly added cards. Values: true | false
}, // Behavior setting. }, // Behavior setting.
"nPlusOne": { "nPlusOne": {
"highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false "highlightEnabled": false, // Enable fast local highlighting for words already known in Anki. Values: true | false
@@ -213,20 +222,20 @@
"decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names. "decks": [], // Decks used for N+1 known-word cache scope. Supports one or more deck names.
"minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3). "minSentenceWords": 3, // Minimum sentence word count required for N+1 targeting (default: 3).
"nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight. "nPlusOne": "#c6a0f6", // Color used for the single N+1 target token highlight.
"knownWord": "#a6da95", // Color used for legacy known-word highlights. "knownWord": "#a6da95" // Color used for legacy known-word highlights.
}, // N plus one setting. }, // N plus one setting.
"metadata": { "metadata": {
"pattern": "[SubMiner] %f (%t)", // Pattern setting. "pattern": "[SubMiner] %f (%t)" // Pattern setting.
}, // Metadata setting. }, // Metadata setting.
"isLapis": { "isLapis": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
"sentenceCardModel": "Japanese sentences", // Sentence card model setting. "sentenceCardModel": "Japanese sentences" // Sentence card model setting.
}, // Is lapis setting. }, // Is lapis setting.
"isKiku": { "isKiku": {
"enabled": false, // Enabled setting. Values: true | false "enabled": false, // Enabled setting. Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled "fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
"deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false "deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
}, // Is kiku setting. } // Is kiku setting.
}, // Automatic Anki updates and media generation options. }, // Automatic Anki updates and media generation options.
// ========================================== // ==========================================
@@ -236,7 +245,7 @@
"jimaku": { "jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting. "apiBaseUrl": "https://jimaku.cc", // Api base url setting.
"languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none "languagePreference": "ja", // Preferred language used in Jimaku search. Values: ja | en | none
"maxEntryResults": 10, // Maximum Jimaku search results returned. "maxEntryResults": 10 // Maximum Jimaku search results returned.
}, // Jimaku API configuration and defaults. }, // Jimaku API configuration and defaults.
// ========================================== // ==========================================
@@ -247,7 +256,10 @@
"mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off "mode": "automatic", // YouTube subtitle generation mode for the launcher script. Values: automatic | preprocess | off
"whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine. "whisperBin": "", // Path to whisper.cpp CLI used as fallback transcription engine.
"whisperModel": "", // Path to whisper model used for fallback transcription. "whisperModel": "", // Path to whisper model used for fallback transcription.
"primarySubLanguages": ["ja", "jpn"], // Comma-separated primary subtitle language priority used by the launcher. "primarySubLanguages": [
"ja",
"jpn"
] // Comma-separated primary subtitle language priority used by the launcher.
}, // Defaults for subminer YouTube subtitle extraction/transcription mode. }, // Defaults for subminer YouTube subtitle extraction/transcription mode.
// ========================================== // ==========================================
@@ -256,7 +268,7 @@
// ========================================== // ==========================================
"anilist": { "anilist": {
"enabled": false, // Enable AniList post-watch progress updates. Values: true | false "enabled": false, // Enable AniList post-watch progress updates. Values: true | false
"accessToken": "", // Optional explicit AniList access token override; leave empty to use locally stored token from setup. "accessToken": "" // Optional explicit AniList access token override; leave empty to use locally stored token from setup.
}, // Anilist API credentials and update behavior. }, // Anilist API credentials and update behavior.
// ========================================== // ==========================================
@@ -280,8 +292,16 @@
"pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false "pullPictures": false, // Enable Jellyfin poster/icon fetching for launcher menus. Values: true | false
"iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons. "iconCacheDir": "/tmp/subminer-jellyfin-icons", // Directory used by launcher for cached Jellyfin poster icons.
"directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false "directPlayPreferred": true, // Try direct play before server-managed transcoding when possible. Values: true | false
"directPlayContainers": ["mkv", "mp4", "webm", "mov", "flac", "mp3", "aac"], // Container allowlist for direct play decisions. "directPlayContainers": [
"transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable. "mkv",
"mp4",
"webm",
"mov",
"flac",
"mp3",
"aac"
], // Container allowlist for direct play decisions.
"transcodeVideoCodec": "h264" // Preferred transcode video codec when direct play is unavailable.
}, // Optional Jellyfin integration for auth, browsing, and playback launch. }, // Optional Jellyfin integration for auth, browsing, and playback launch.
// ========================================== // ==========================================
@@ -292,7 +312,7 @@
"discordPresence": { "discordPresence": {
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false "enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
"updateIntervalMs": 3000, // Minimum interval between presence payload updates. "updateIntervalMs": 3000, // Minimum interval between presence payload updates.
"debounceMs": 750, // Debounce delay used to collapse bursty presence updates. "debounceMs": 750 // Debounce delay used to collapse bursty presence updates.
}, // Optional Discord Rich Presence activity card updates for current playback/study session. }, // Optional Discord Rich Presence activity card updates for current playback/study session.
// ========================================== // ==========================================
@@ -314,7 +334,7 @@
"telemetryDays": 30, // Telemetry retention window in days. "telemetryDays": 30, // Telemetry retention window in days.
"dailyRollupsDays": 365, // Daily rollup retention window in days. "dailyRollupsDays": 365, // Daily rollup retention window in days.
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days. "monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
"vacuumIntervalDays": 7, // Minimum days between VACUUM runs. "vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
}, // Retention setting. } // Retention setting.
}, // Enable/disable immersion tracking. } // Enable/disable immersion tracking.
} }

View File

@@ -95,4 +95,6 @@ Read first. Keep concise.
| `codex-docs-video-thumb-cache-20260223T033929Z-k8p2` | `codex-docs-video-thumb-cache` | `Fix docs landing page demo video thumbnail staleness after direct asset replacement.` | `handoff` | `docs/subagents/agents/codex-docs-video-thumb-cache-20260223T033929Z-k8p2.md` | `2026-02-23T03:44:04Z` | | `codex-docs-video-thumb-cache-20260223T033929Z-k8p2` | `codex-docs-video-thumb-cache` | `Fix docs landing page demo video thumbnail staleness after direct asset replacement.` | `handoff` | `docs/subagents/agents/codex-docs-video-thumb-cache-20260223T033929Z-k8p2.md` | `2026-02-23T03:44:04Z` |
| `codex-development-docs-review-20260223T034520Z-2ebb` | `codex-development-docs-review` | `Review codebase and refresh docs/development.md to match current project state.` | `done` | `docs/subagents/agents/codex-development-docs-review-20260223T034520Z-2ebb.md` | `2026-02-23T03:49:16Z` | | `codex-development-docs-review-20260223T034520Z-2ebb` | `codex-development-docs-review` | `Review codebase and refresh docs/development.md to match current project state.` | `done` | `docs/subagents/agents/codex-development-docs-review-20260223T034520Z-2ebb.md` | `2026-02-23T03:49:16Z` |
| `opencode-bun-migration-20260223T043000Z-k9m2` | `opencode-bun-migration` | `Execute TASK-115 Bun-only migration plan: parity map, dist/utility script migration, CI/docs cutover.` | `handoff` | `docs/subagents/agents/opencode-bun-migration-20260223T043000Z-k9m2.md` | `2026-02-23T04:36:00Z` | | `opencode-bun-migration-20260223T043000Z-k9m2` | `opencode-bun-migration` | `Execute TASK-115 Bun-only migration plan: parity map, dist/utility script migration, CI/docs cutover.` | `handoff` | `docs/subagents/agents/opencode-bun-migration-20260223T043000Z-k9m2.md` | `2026-02-23T04:36:00Z` |
| `opencode-initial-release-plan-20260223T044059Z-p7k2` | `opencode-initial-release-plan` | `Analyze main history and draft copy/paste initial-release history-cleanup plan.` | `planning` | `docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md` | `2026-02-23T04:40:59Z` | | `opencode-initial-release-plan-20260223T044059Z-p7k2` | `opencode-initial-release-plan` | `Analyze main history and draft copy/paste initial-release history-cleanup plan.` | `handoff` | `docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md` | `2026-02-23T04:47:30Z` |
| `codex-plugin-start-overlay-gate-20260223T044347Z-k3n8` | `codex-plugin-start-overlay-gate` | `Fix mpv plugin regression where start is refused if SubMiner process is not already running (TASK-117).` | `handoff` | `docs/subagents/agents/codex-plugin-start-overlay-gate-20260223T044347Z-k3n8.md` | `2026-02-23T04:47:40Z` |
| `codex-commit-changes-20260223T050731Z-rer4` | `codex-commit-changes` | `Commit current working tree changes with a conventional commit message derived from content.` | `in_progress` | `docs/subagents/agents/codex-commit-changes-20260223T050731Z-rer4.md` | `2026-02-23T05:07:31Z` |

View File

@@ -0,0 +1,26 @@
# Agent: `codex-commit-changes-20260223T050731Z-rer4`
- alias: `codex-commit-changes`
- mission: `Commit current working tree changes requested by user with a content-derived conventional commit message.`
- status: `in_progress`
- branch: `main`
- started_at: `2026-02-23T05:07:31Z`
- heartbeat_minutes: `5`
## Current Work (newest first)
- [2026-02-23T05:07:31Z] intent: inspect diff scope, preserve existing user/agent edits, stage all current changes, and commit once with conventional format.
## Files Touched
- `docs/subagents/INDEX.md`
- `docs/subagents/collaboration.md`
- `docs/subagents/agents/codex-commit-changes-20260223T050731Z-rer4.md`
## Assumptions
- User request "commit changes" means commit all current working-tree changes.
- No additional code edits are required beyond required coordination bookkeeping.
## Open Questions / Blockers
- None.
## Next Step
- Stage all changes and create one conventional commit message based on the diff.

View File

@@ -0,0 +1,30 @@
# Agent: `codex-plugin-start-overlay-gate-20260223T044347Z-k3n8`
- alias: `codex-plugin-start-overlay-gate`
- mission: `Fix mpv plugin regression where start is refused if SubMiner process is not already running (TASK-117).`
- status: `handoff`
- branch: `main`
- started_at: `2026-02-23T04:49:00Z`
- heartbeat_minutes: `5`
## Current Work (newest first)
- [2026-02-23T04:47:40Z] completed: patched cold-start gate in `plugin/subminer.lua`, added Lua regression harness, validated red->green + syntax + focused launcher mpv tests.
- [2026-02-23T04:49:00Z] intent: reproduce root cause from user logs; add red test first; patch plugin start gate with minimal behavior change.
## Files Touched
- `plugin/subminer.lua`
- `scripts/test-plugin-start-gate.lua`
- `backlog/tasks/task-117 - Fix-mpv-plugin-overlay-start-gate-when-SubMiner-is-not-running.md`
- `docs/subagents/INDEX.md`
- `docs/subagents/collaboration.md`
- `docs/subagents/agents/codex-plugin-start-overlay-gate-20260223T044347Z-k3n8.md`
## Assumptions
- Regression introduced by `is_subminer_ipc_ready()` check at start of `start_overlay()`.
- Cold-start should not require pre-existing process; socket checks remain valid once process exists.
## Open Questions / Blockers
- None.
## Next Step
- User runtime verification: launch with `SUBMINER_APPIMAGE_PATH=... ./dist/launcher/subminer --log-level debug ...` and trigger `y-s`.

View File

@@ -0,0 +1,35 @@
# Agent: `opencode-initial-release-plan-20260223T044059Z-p7k2`
- alias: `opencode-initial-release-plan`
- mission: `Analyze main history and draft copy/paste initial-release history-cleanup plan.`
- status: `handoff`
- branch: `main`
- started_at: `2026-02-23T04:40:59Z`
- heartbeat_minutes: `5`
## Current Work (newest first)
- [2026-02-23T04:47:30Z] handoff: added `initial-release.md` with orphan-branch + curated-commit cutover commands, backup refs, validation checks, force-with-lease push, and team resync instructions.
- [2026-02-23T04:44:30Z] progress: analyzed `main` history (325 commits; high refactor/noise skew) and confirmed release-shaped repo layout for snapshot regrouping.
- [2026-02-23T04:40:59Z] intent: inspect commit history and current repository state, then write `initial-release.md` with exact cutover commands.
## Files Touched
- `docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md`
- `docs/subagents/INDEX.md`
- `docs/subagents/collaboration.md`
- `initial-release.md`
- `backlog/tasks/task-116 - Analyze-git-history-and-draft-initial-release-cleanup-plan.md`
## Assumptions
- history rewrite is allowed because repo is private and pre-release.
- recommendation should prefer low-risk deterministic workflow over complex interactive rebase.
## Open Questions / Blockers
- none.
## Next Step
- share recommendation + path to `initial-release.md`; execute cutover during freeze window.

View File

@@ -148,6 +148,8 @@ Shared notes. Append-only.
## 2026-02-23 ## 2026-02-23
- [2026-02-23T04:49:00Z] [codex-plugin-start-overlay-gate-20260223T044347Z-k3n8|codex-plugin-start-overlay-gate] overlap note: touching `plugin/subminer.lua` start gate path for cold-start regression (`Refusing to start overlay: SubMiner process not running`); adding TASK-117 + focused regression harness.
- [2026-02-23T04:47:40Z] [codex-plugin-start-overlay-gate-20260223T044347Z-k3n8|codex-plugin-start-overlay-gate] completed TASK-117: `start_overlay()` now allows cold-start when process absent; added `scripts/test-plugin-start-gate.lua` red/green regression harness; verified with `luac -p plugin/subminer.lua` and `bun test launcher/mpv.test.ts`.
- [2026-02-23T01:10:27Z] [opencode-task109-discord-presence-20260223T011027Z-j9r4|opencode-task109-discord-presence] starting TASK-109 closure pass via Backlog MCP + writing-plans/executing-plans; scope validate existing Discord config/runtime/docs changes, close remaining DoD evidence, and finalize task status if gates pass. - [2026-02-23T01:10:27Z] [opencode-task109-discord-presence-20260223T011027Z-j9r4|opencode-task109-discord-presence] starting TASK-109 closure pass via Backlog MCP + writing-plans/executing-plans; scope validate existing Discord config/runtime/docs changes, close remaining DoD evidence, and finalize task status if gates pass.
- [2026-02-23T01:15:39Z] [opencode-task109-discord-presence-20260223T011027Z-j9r4|opencode-task109-discord-presence] user feedback from real Discord session: status resumed to Playing with noticeable delay; tuned default `discordPresence.updateIntervalMs` from 15000 to 3000 in defaults/docs/examples and updated focused config expectations; reran focused config + discord presence tests green. - [2026-02-23T01:15:39Z] [opencode-task109-discord-presence-20260223T011027Z-j9r4|opencode-task109-discord-presence] user feedback from real Discord session: status resumed to Playing with noticeable delay; tuned default `discordPresence.updateIntervalMs` from 15000 to 3000 in defaults/docs/examples and updated focused config expectations; reran focused config + discord presence tests green.
- [2026-02-23T01:27:55Z] [codex-task88-yomitan-flow-20260223T012755Z-x4m2|codex-task88-yomitan-flow] starting TASK-88 via Backlog MCP + writing-plans/executing-plans; expected overlap in tokenizer modules (`src/core/services/tokenizer*`, Yomitan flow wiring/tests); will keep scope to MeCab fallback removal and token flow simplification. - [2026-02-23T01:27:55Z] [codex-task88-yomitan-flow-20260223T012755Z-x4m2|codex-task88-yomitan-flow] starting TASK-88 via Backlog MCP + writing-plans/executing-plans; expected overlap in tokenizer modules (`src/core/services/tokenizer*`, Yomitan flow wiring/tests); will keep scope to MeCab fallback removal and token flow simplification.
@@ -169,3 +171,5 @@ Shared notes. Append-only.
- [2026-02-23T04:30:00Z] [opencode-bun-migration-20260223T043000Z-k9m2|opencode-bun-migration] starting TASK-115 Bun-only migration execution; initial scope `package.json`, CI/release workflows, and setup docs to remove Node requirements after parity checks. - [2026-02-23T04:30:00Z] [opencode-bun-migration-20260223T043000Z-k9m2|opencode-bun-migration] starting TASK-115 Bun-only migration execution; initial scope `package.json`, CI/release workflows, and setup docs to remove Node requirements after parity checks.
- [2026-02-23T04:36:00Z] [opencode-bun-migration-20260223T043000Z-k9m2|opencode-bun-migration] completed TASK-115 Bun-only migration pass: dist/utility commands moved off direct Node invocation, CI/release Node setup removed, Bun parity matrix + docs updates landed, full Bun validation gate suite passed, and TASK-115 + child tasks finalized Done in Backlog. - [2026-02-23T04:36:00Z] [opencode-bun-migration-20260223T043000Z-k9m2|opencode-bun-migration] completed TASK-115 Bun-only migration pass: dist/utility commands moved off direct Node invocation, CI/release Node setup removed, Bun parity matrix + docs updates landed, full Bun validation gate suite passed, and TASK-115 + child tasks finalized Done in Backlog.
- [2026-02-23T04:40:59Z] [opencode-initial-release-plan-20260223T044059Z-p7k2|opencode-initial-release-plan] starting user-requested release-history cleanup planning pass; scope git-history analysis + current-state review + `initial-release.md` command playbook. - [2026-02-23T04:40:59Z] [opencode-initial-release-plan-20260223T044059Z-p7k2|opencode-initial-release-plan] starting user-requested release-history cleanup planning pass; scope git-history analysis + current-state review + `initial-release.md` command playbook.
- [2026-02-23T04:47:30Z] [opencode-initial-release-plan-20260223T044059Z-p7k2|opencode-initial-release-plan] completed planning pass: analyzed commit history/current state and added `initial-release.md` with recommended orphan-history + 7-commit split plan, validation, cutover, and teammate resync commands.
- [2026-02-23T05:07:31Z] [codex-commit-changes-20260223T050731Z-rer4|codex-commit-changes] starting user-requested commit pass for current working tree; scope is stage all existing tracked/untracked changes and create conventional commit without additional behavior edits.

167
initial-release.md Normal file
View File

@@ -0,0 +1,167 @@
# Initial Release Git History Cleanup Plan
## Recommendation
Given the current `main` history shape (325 commits, heavy refactor churn, and many internal maintenance commits), the best pre-release cleanup path is:
1. Preserve the old `main` history in remote backup refs.
2. Build a **new orphan history** from the current project snapshot.
3. Recreate history as **7 curated commits** grouped by release-facing domains.
4. Verify tree equality with old `main` and replace `origin/main` once.
This avoids fragile `rebase -i --root` over 300+ commits while still keeping meaningful archaeology for future maintenance.
## Why this is best for this repo
- Current history is dominated by refactor/noise commits (`refactor`: 138, `other`: 91).
- Commit velocity is very high and bursty (multiple 30-45 commit days), so interactive cleanup is risky/time-expensive.
- Codebase is already mature and release-shaped (Electron app + launcher + plugin + docs + CI), so snapshot regrouping is cleaner.
- You are pre-release and private; a one-time force update is low risk if coordinated.
---
## Pre-cutover checklist
Run from repository root.
```bash
git checkout main
git pull --ff-only origin main
```
Coordinate freeze:
- Pause merges/pushes to `main`.
- Ask teammates to stop rebasing onto `main` during cutover.
---
## Copy/paste command sequence
### 1) Safety backups (local + remote)
```bash
set -euo pipefail
git checkout main
git pull --ff-only origin main
OLD_MAIN_COMMIT="$(git rev-parse main)"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
echo "Old main commit: ${OLD_MAIN_COMMIT}"
echo "Cutover stamp: ${STAMP}"
git tag "pre-release-history-backup-${STAMP}" "${OLD_MAIN_COMMIT}"
git branch "archive/pre-release-history-${STAMP}" "${OLD_MAIN_COMMIT}"
git push origin "pre-release-history-backup-${STAMP}"
git push origin "archive/pre-release-history-${STAMP}"
```
### 2) Handle dirty workspace safely
If `git status --short` is not empty:
```bash
git stash push -u -m "initial-release-history-cleanup-${STAMP}"
```
### 3) Build new clean history on orphan branch
```bash
git checkout --orphan release/main-clean
git reset
```
Now create curated commits.
```bash
# Commit 1: repository scaffolding and build toolchain
git add .gitignore .gitmodules .npmrc .prettierignore .prettierrc.json AGENTS.md LICENSE Makefile package.json bun.lock tsconfig.json tsconfig.renderer.json build scripts .github
git commit -m "chore: bootstrap repository tooling and release automation"
# Commit 2: core desktop app runtime
git add src
git commit -m "feat(core): add Electron runtime, services, and app composition"
# Commit 3: launcher and mpv plugin integration
git add launcher plugin subminer
git commit -m "feat(integration): add launcher and mpv plugin workflows"
# Commit 4: bundled assets and vendor payloads
git add assets vendor
git commit -m "feat(assets): bundle runtime assets and vendor dependencies"
# Commit 5: test suites and verification harnesses
git add tests
git commit -m "test: add regression suites and harness coverage"
# Commit 6: documentation and examples
git add README.md docs config.example.jsonc
git commit -m "docs: add setup guides, architecture docs, and config examples"
# Commit 7: project/backlog metadata and remaining files (if any)
git add -A
if ! git diff --cached --quiet; then
git commit -m "chore: add project management metadata and remaining repository files"
fi
```
### 4) Validate tree equivalence to old main snapshot
```bash
git diff --stat "${OLD_MAIN_COMMIT}^{tree}" HEAD^{tree}
git diff --name-status "${OLD_MAIN_COMMIT}^{tree}" HEAD^{tree}
```
Expected: no output. If output exists, stop and inspect before pushing.
### 5) Replace `origin/main`
```bash
git push --force-with-lease origin release/main-clean:main
```
### 6) Tag cleaned initial-release baseline
```bash
git fetch origin
git checkout main
git reset --hard origin/main
git tag "v0.1.0-initial-clean-history"
git push origin "v0.1.0-initial-clean-history"
```
### 7) Restore local stashed work (if stashed earlier)
```bash
git stash list
git stash pop
```
---
## Team resync message (send after cutover)
```text
main history was rewritten for initial release cleanup.
If you have no local work:
git fetch origin
git checkout main
git reset --hard origin/main
If you have local work:
git fetch origin
git checkout -b backup/<name>-before-main-rewrite
# then rebase/cherry-pick your feature commits onto new origin/main
```
---
## Notes
- This plan intentionally avoids `git rebase -i --root` to reduce conflict risk.
- `--force-with-lease` is mandatory safety rail.
- Keep the backup tag/branch until after public release stabilization.

View File

@@ -1526,7 +1526,8 @@ end
local function start_overlay(overrides) local function start_overlay(overrides)
local socket_ready, reason = is_subminer_ipc_ready() local socket_ready, reason = is_subminer_ipc_ready()
if not socket_ready then local process_not_running = reason == "SubMiner process not running"
if not socket_ready and not process_not_running then
subminer_log("warn", "process", "Refusing to start overlay: " .. tostring(reason)) subminer_log("warn", "process", "Refusing to start overlay: " .. tostring(reason))
show_osd("SubMiner IPC not set up. Launch mpv with --input-ipc-server=/tmp/subminer-socket") show_osd("SubMiner IPC not set up. Launch mpv with --input-ipc-server=/tmp/subminer-socket")
return return

View File

@@ -0,0 +1,193 @@
local function run_plugin_scenario(config)
config = config or {}
local recorded = {
async_calls = {},
script_messages = {},
osd = {},
logs = {},
}
local function make_mp_stub()
local mp = {}
function mp.get_property(name)
if name == "platform" then
return config.platform or "linux"
end
if name == "filename/no-ext" then
return config.filename_no_ext or ""
end
if name == "filename" then
return config.filename or ""
end
if name == "path" then
return config.path or ""
end
if name == "media-title" then
return config.media_title or ""
end
return ""
end
function mp.get_property_native(_name)
return config.chapter_list or {}
end
function mp.command_native(command)
local args = command.args or {}
if args[1] == "ps" then
return {
status = 0,
stdout = config.process_list or "",
stderr = "",
}
end
if args[1] == "curl" then
return { status = 0, stdout = "{}", stderr = "" }
end
return { status = 0, stdout = "", stderr = "" }
end
function mp.command_native_async(command, callback)
recorded.async_calls[#recorded.async_calls + 1] = command
if callback then
callback(true, { status = 0, stdout = "", stderr = "" }, nil)
end
end
function mp.add_timeout(_seconds, callback)
if callback then
callback()
end
end
function mp.register_script_message(name, fn)
recorded.script_messages[name] = fn
end
function mp.add_key_binding(_keys, _name, _fn) end
function mp.register_event(_name, _fn) end
function mp.add_hook(_name, _prio, _fn) end
function mp.observe_property(_name, _kind, _fn) end
function mp.osd_message(message, _duration)
recorded.osd[#recorded.osd + 1] = message
end
function mp.get_time()
return 0
end
function mp.commandv(...) end
function mp.set_property_native(...) end
function mp.get_script_name()
return "subminer"
end
return mp
end
local mp = make_mp_stub()
local options = {}
local utils = {}
function options.read_options(target, _name)
if config.socket_path then
target.socket_path = config.socket_path
end
end
function utils.file_info(path)
local exists = config.files and config.files[path]
if exists then
return { is_dir = false }
end
return nil
end
function utils.join_path(...)
local parts = { ... }
return table.concat(parts, "/")
end
function utils.parse_json(_json)
return {}, nil
end
package.loaded["mp"] = nil
package.loaded["mp.input"] = nil
package.loaded["mp.msg"] = nil
package.loaded["mp.options"] = nil
package.loaded["mp.utils"] = nil
package.preload["mp"] = function()
return mp
end
package.preload["mp.input"] = function()
return {
select = function(_) end,
}
end
package.preload["mp.msg"] = function()
return {
info = function(line)
recorded.logs[#recorded.logs + 1] = line
end,
warn = function(line)
recorded.logs[#recorded.logs + 1] = line
end,
error = function(line)
recorded.logs[#recorded.logs + 1] = line
end,
debug = function(line)
recorded.logs[#recorded.logs + 1] = line
end,
}
end
package.preload["mp.options"] = function()
return options
end
package.preload["mp.utils"] = function()
return utils
end
local ok, err = pcall(dofile, "plugin/subminer.lua")
if not ok then
return nil, err, recorded
end
return recorded, nil, recorded
end
local function assert_true(condition, message)
if condition then
return
end
error(message)
end
local function find_start_call(async_calls)
for _, call in ipairs(async_calls) do
local args = call.args or {}
for i = 1, #args do
if args[i] == "--start" then
return true
end
end
end
return false
end
local binary_path = "/tmp/subminer-binary"
do
local recorded, err = run_plugin_scenario({
process_list = "",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for cold-start scenario: " .. tostring(err))
assert_true(recorded.script_messages["subminer-start"] ~= nil, "subminer-start script message not registered")
recorded.script_messages["subminer-start"]("texthooker=no")
assert_true(find_start_call(recorded.async_calls), "expected cold-start to invoke --start command when process is absent")
end
print("plugin start gate regression tests: OK")