diff --git a/backlog/tasks/task-116 - Analyze-git-history-and-draft-initial-release-cleanup-plan.md b/backlog/tasks/task-116 - Analyze-git-history-and-draft-initial-release-cleanup-plan.md
new file mode 100644
index 0000000..4587eea
--- /dev/null
+++ b/backlog/tasks/task-116 - Analyze-git-history-and-draft-initial-release-cleanup-plan.md
@@ -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
+
+
+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.
+
+
+## Acceptance Criteria
+
+- [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
+
+
+## Final Summary
+
+
+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.
+
diff --git a/backlog/tasks/task-117 - Fix-mpv-plugin-overlay-start-gate-when-SubMiner-is-not-running.md b/backlog/tasks/task-117 - Fix-mpv-plugin-overlay-start-gate-when-SubMiner-is-not-running.md
new file mode 100644
index 0000000..04e47b2
--- /dev/null
+++ b/backlog/tasks/task-117 - Fix-mpv-plugin-overlay-start-gate-when-SubMiner-is-not-running.md
@@ -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
+
+
+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.
+
+
+## Action Steps
+
+
+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.
+
+
+## Acceptance Criteria
+
+- [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.
+
+
+## Implementation Notes
+
+
+- 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`.
+
+
+## Final Summary
+
+
+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.
+
diff --git a/config.example.jsonc b/config.example.jsonc
index f57972e..f7f4926 100644
--- a/config.example.jsonc
+++ b/config.example.jsonc
@@ -5,6 +5,7 @@
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/
{
+
// ==========================================
// Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
@@ -23,7 +24,7 @@
// Control whether browser opens automatically for 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.
// ==========================================
@@ -33,7 +34,7 @@
// ==========================================
"websocket": {
"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.
// ==========================================
@@ -42,7 +43,7 @@
// Set to debug for full runtime diagnostics.
// ==========================================
"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.
// ==========================================
@@ -64,7 +65,7 @@
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card 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.
// ==========================================
@@ -74,7 +75,7 @@
// This edit-mode shortcut is fixed and is not currently configurable.
// ==========================================
"invisibleOverlay": {
- "startupVisibility": "platform-default", // Startup visibility setting.
+ "startupVisibility": "platform-default" // Startup visibility setting.
}, // Startup behavior for the invisible interactive subtitle mining layer.
// ==========================================
@@ -94,7 +95,7 @@
"secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
- "defaultMode": "hover", // Default mode setting.
+ "defaultMode": "hover" // Default mode setting.
}, // Dual subtitle track options.
// ==========================================
@@ -105,7 +106,7 @@
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Alass path setting.
"ffsubsync_path": "", // Ffsubsync path setting.
- "ffmpeg_path": "", // Ffmpeg path setting.
+ "ffmpeg_path": "" // Ffmpeg path setting.
}, // Subsync engine and executable paths.
// ==========================================
@@ -113,7 +114,7 @@
// Initial vertical subtitle position from the bottom.
// ==========================================
"subtitlePosition": {
- "yPercent": 10, // Y percent setting.
+ "yPercent": 10 // Y percent setting.
}, // Initial vertical subtitle position from the bottom.
// ==========================================
@@ -138,7 +139,7 @@
"N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting.
"N4": "#a6e3a1", // N4 setting.
- "N5": "#8aadf4", // N5 setting.
+ "N5": "#8aadf4" // N5 setting.
}, // Jlpt colors setting.
"frequencyDictionary": {
"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).
"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`.
- "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.
"secondary": {
"fontSize": 24, // Font size setting.
@@ -154,8 +161,8 @@
"backgroundColor": "transparent", // Background color setting.
"fontWeight": "normal", // Font weight 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.
- }, // Secondary 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.
}, // Primary and secondary subtitle styling.
// ==========================================
@@ -168,13 +175,15 @@
"enabled": false, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting.
"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": {
"audio": "ExpressionAudio", // Audio setting.
"image": "Picture", // Image setting.
"sentence": "Sentence", // Sentence setting.
"miscInfo": "MiscInfo", // Misc info setting.
- "translation": "SelectionText", // Translation setting.
+ "translation": "SelectionText" // Translation setting.
}, // Fields setting.
"ai": {
"enabled": false, // Enabled setting. Values: true | false
@@ -183,7 +192,7 @@
"model": "openai/gpt-4o-mini", // Model setting.
"baseUrl": "https://openrouter.ai/api", // Base url 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.
"media": {
"generateAudio": true, // Generate audio setting. Values: true | false
@@ -196,7 +205,7 @@
"animatedCrf": 35, // Animated crf setting.
"audioPadding": 0.5, // Audio padding setting.
"fallbackDuration": 3, // Fallback duration setting.
- "maxMediaDuration": 30, // Max media duration setting.
+ "maxMediaDuration": 30 // Max media duration setting.
}, // Media setting.
"behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
@@ -204,7 +213,7 @@
"mediaInsertMode": "append", // Media insert mode setting.
"highlightWord": true, // Highlight word setting. Values: true | false
"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.
"nPlusOne": {
"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.
"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.
- "knownWord": "#a6da95", // Color used for legacy known-word highlights.
+ "knownWord": "#a6da95" // Color used for legacy known-word highlights.
}, // N plus one setting.
"metadata": {
- "pattern": "[SubMiner] %f (%t)", // Pattern setting.
+ "pattern": "[SubMiner] %f (%t)" // Pattern setting.
}, // Metadata setting.
"isLapis": {
"enabled": false, // Enabled setting. Values: true | false
- "sentenceCardModel": "Japanese sentences", // Sentence card model setting.
+ "sentenceCardModel": "Japanese sentences" // Sentence card model setting.
}, // Is lapis setting.
"isKiku": {
"enabled": false, // Enabled setting. Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
- "deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
- }, // Is kiku setting.
+ "deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
+ } // Is kiku setting.
}, // Automatic Anki updates and media generation options.
// ==========================================
@@ -236,7 +245,7 @@
"jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
"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.
// ==========================================
@@ -247,7 +256,10 @@
"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.
"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.
// ==========================================
@@ -256,7 +268,7 @@
// ==========================================
"anilist": {
"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.
// ==========================================
@@ -280,8 +292,16 @@
"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.
"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.
- "transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
+ "directPlayContainers": [
+ "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.
// ==========================================
@@ -292,7 +312,7 @@
"discordPresence": {
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
"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.
// ==========================================
@@ -314,7 +334,7 @@
"telemetryDays": 30, // Telemetry retention window in days.
"dailyRollupsDays": 365, // Daily rollup retention window in days.
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
- "vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
- }, // Retention setting.
- }, // Enable/disable immersion tracking.
+ "vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
+ } // Retention setting.
+ } // Enable/disable immersion tracking.
}
diff --git a/docs/architecture.md b/docs/architecture.md
index 89095b8..0f39e58 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -125,65 +125,108 @@ src/renderer/
## 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
flowchart TD
- classDef entry fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
- classDef comp fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef svc fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef bridge fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef rend fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef ext fill:#a6da95,stroke:#363a4f,color:#24273a,stroke-width:1.5px
+ classDef entry fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
+ classDef comp fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
+ classDef svc fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
+ classDef bridge fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
+ classDef rend fill:#8bd5ca,stroke:#494d64,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/"]
- Startup["Startup & Lifecycle
startup · app-lifecycle
startup-lifecycle · state"]:::comp
- Wiring["Runtime Wiring
ipc-runtime · cli-runtime
overlay-runtime · subsync-runtime"]:::comp
+ direction TB
+ Startup["Startup & Lifecycle
startup · app-lifecycle · startup-lifecycle · state"]:::comp
+ Wiring["Runtime Wiring
ipc-runtime · cli-runtime · overlay-runtime · subsync-runtime"]:::comp
+ Composers["Composers
mpv-runtime · anilist-tracking · jellyfin-runtime"]:::comp
end
subgraph Svc["Services — src/core/services/"]
- direction LR
- Mpv["MPV Stack
transport · protocol
state · properties"]:::svc
- Overlay["Overlay
manager · window
visibility · bridge"]:::svc
- Mining["Mining & Subtitles
mining · field-grouping
subtitle-ws · tokenizer"]:::svc
- Integrations["Integrations
jimaku · subsync
texthooker · yomitan"]:::svc
+ direction TB
+ subgraph SvcRow1[" "]
+ direction LR
+ Mpv["MPV Stack
transport · protocol
properties · render-metrics"]:::svc
+ Overlay["Overlay Manager
window · geometry
visibility · bridge"]:::svc
+ end
+ subgraph SvcRow2[" "]
+ direction LR
+ Mining["Mining & Subtitles
mining · field-grouping
subtitle-ws · tokenizer"]:::svc
+ Integrations["Integrations
jimaku · subsync · texthooker
yomitan · discord-presence"]:::svc
+ end
+ subgraph SvcRow3[" "]
+ direction LR
+ Tracking["Tracking
anilist · jellyfin-remote
immersion-tracker"]:::svc
+ Config["Config & Runtime
config-hot-reload
runtime-options"]:::svc
+ end
end
- Bridge(["preload.ts — Electron IPC"]):::bridge
+ Bridge(["preload.ts — Electron IPC bridge"]):::bridge
subgraph Rend["Renderer — src/renderer/"]
- Orchestration["renderer.ts
orchestration · IPC wiring"]:::rend
- UI["subtitle-render · positioning
handlers · modals"]:::rend
+ direction TB
+ subgraph Windows["Three overlay windows"]
+ direction LR
+ Visible["Visible
interactive Yomitan lookups"]:::rend
+ Invisible["Invisible
mpv-matched positioning"]:::rend
+ Secondary["Secondary
secondary subtitle bar"]:::rend
+ end
+ UI["subtitle-render · positioning · handlers · modals"]:::rend
end
subgraph Ext["External Systems"]
direction LR
- mpv["mpv"]:::ext
- Anki["AnkiConnect"]:::ext
- Jimaku["Jimaku API"]:::ext
- Tracker["Window Tracker"]:::ext
+ mpvExt["mpv player"]:::ext
+ AnkiExt["AnkiConnect"]:::ext
+ JimakuExt["Jimaku API"]:::ext
+ TrackerExt["Window Tracker
Hyprland · Sway · X11 · macOS"]:::ext
+ AnilistExt["AniList API"]:::ext
+ JellyfinExt["Jellyfin"]:::ext
+ DiscordExt["Discord RPC"]:::ext
end
- Main -->|delegates| Comp
- Startup -->|initializes| Svc
- Wiring -->|dispatches to| Svc
+ subgraph ExtRt["External Runtimes"]
+ direction LR
+ Launcher["launcher/
CLI command dispatch"]:::extrt
+ Plugin["subminer.lua
mpv plugin"]:::extrt
+ end
- Overlay <--> Bridge
+ Main -->|"delegates"| Comp
+ Startup -->|"initializes"| Svc
+ Wiring -->|"dispatches to"| Svc
+ Composers -->|"wires"| Svc
+
+ Overlay <-->Bridge
Mining <--> Bridge
- Bridge <--> Orchestration
- Orchestration --> UI
+ Bridge <--> Visible
+ Bridge <--> Invisible
+ Bridge <--> Secondary
+ Windows --> UI
- Mpv <-->|JSON socket| mpv
- Mining -->|HTTP| Anki
- Integrations -->|HTTP| Jimaku
- Overlay --> Tracker
+ Mpv <-->|"JSON IPC socket"| mpvExt
+ Mining -->|"HTTP"| AnkiExt
+ Integrations -->|"HTTP"| JimakuExt
+ 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 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 Windows fill:#1e2030,stroke:#494d64,color:#cad3f5
style Ext fill:#363a4f,stroke:#494d64,color:#cad3f5
+ style ExtRt fill:#363a4f,stroke:#494d64,color:#cad3f5
```
## Composition Pattern
@@ -242,52 +285,81 @@ For domains migrated to reducer-style transitions (for example AniList token/que
## 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.
-- **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).
-- **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.
-- **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.
-- **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.
+- **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.
+- **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.
+- **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 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.
+- **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
-flowchart TD
- classDef start fill:#c6a0f6,stroke:#363a4f,color:#24273a,stroke-width:2px
- classDef phase fill:#b7bdf8,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef decision fill:#f5a97f,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef init fill:#8aadf4,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef runtime fill:#8bd5ca,stroke:#363a4f,color:#24273a,stroke-width:1.5px
- classDef shutdown fill:#ed8796,stroke:#363a4f,color:#24273a,stroke-width:1.5px
+flowchart LR
+ classDef start fill:#c6a0f6,stroke:#494d64,color:#24273a,stroke-width:2px,font-weight:bold
+ classDef phase fill:#b7bdf8,stroke:#494d64,color:#24273a,stroke-width:1.5px
+ classDef decision fill:#f5a97f,stroke:#494d64,color:#24273a,stroke-width:1.5px
+ classDef init fill:#8aadf4,stroke:#494d64,color:#24273a,stroke-width:1.5px
+ classDef runtime fill:#8bd5ca,stroke:#494d64,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 --> Parse["startup.ts
Parse argv · detect backend · resolve config"]:::phase
- Parse --> GenCheck{"--generate-config?"}:::decision
- GenCheck -->|yes| GenExit["Write config template & exit"]:::phase
- GenCheck -->|no| Lifecycle["app-lifecycle.ts
Acquire single-instance lock
Register Electron lifecycle hooks"]:::phase
- Lifecycle -->|"app.whenReady()"| Ready["startup-lifecycle.ts"]:::phase
+ CLI["CLI args &
environment"]:::start
+ CLI --> Proto["Module-level init
register protocols
construct services
wire deps"]:::phase
+ Proto --> Parse["startup.ts
parse argv
detect backend"]:::phase
+ Parse --> GenCheck{"--generate
-config?"}:::decision
+ GenCheck -->|"yes"| GenExit["Write template
& exit"]:::phase
+ GenCheck -->|"no"| Lock["app-lifecycle.ts
single-instance lock
lifecycle hooks"]:::phase
- Ready --> Init
- subgraph Init["Initialization"]
- direction LR
- Config["Load config
resolve keybindings"]:::init
- Runtime["Create mpv client
init runtime options"]:::init
- Platform["Start window tracker
WebSocket policy"]:::init
- end
+ Lock -->|"app.whenReady()"| Ready["composeAppReady
Runtime()"]:::phase
- Init --> Create["Create overlay window
Establish IPC bridge"]:::phase
- Create --> Warm["Background warmups
MeCab · Yomitan · dictionaries · Jellyfin"]:::phase
+ Ready --> Config["Config reload
keybindings
log level"]:::init
+ Ready --> MpvInit["MpvIpcClient
connect socket
subscribe 26 props"]:::init
+ Ready --> Platform["RuntimeOptions
timing tracker
immersion tracker"]:::init
+
+ Config --> OverlayInit
+ MpvInit --> OverlayInit
+ Platform --> OverlayInit
+
+ OverlayInit["initializeOverlay
Runtime()"]:::phase
+
+ OverlayInit --> VisWin["Visible window
Yomitan lookups"]:::init
+ OverlayInit --> InvWin["Invisible window
mpv positioning"]:::init
+ OverlayInit --> SecWin["Secondary window
subtitle bar"]:::init
+ OverlayInit --> Shortcuts["Register global
shortcuts"]:::init
+
+ VisWin --> Warmups
+ InvWin --> Warmups
+ SecWin --> Warmups
+ Shortcuts --> Warmups
+
+ Warmups["Background
warmups"]:::phase
+
+ Warmups --> W1["MeCab"]:::warmup
+ Warmups --> W2["Yomitan"]:::warmup
+ Warmups --> W3["JLPT + freq
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"]
- direction LR
- Events["mpv · IPC · CLI
shortcut events"]:::runtime
- Dispatch["Route to service
via composition layer"]:::runtime
- State["Update state
broadcast to renderer"]:::runtime
- Events --> Dispatch --> State
+ direction TB
+ MpvEvt["mpv events: subtitle · timing · metrics"]:::runtime
+ IpcEvt["IPC: renderer requests · CLI commands"]:::runtime
+ ExtEvt["Shortcuts · config hot-reload"]:::runtime
+ MpvEvt & IpcEvt & ExtEvt --> Route["Route via composers"]:::runtime
+ Route --> Process["SubtitlePipeline
normalize → tokenize → merge"]:::runtime
+ Process --> Broadcast["Update AppState
broadcast to windows"]:::runtime
end
- Loop -->|"app close"| Quit["Electron will-quit"]:::shutdown
- Quit --> Teardown["Close mpv socket · unregister shortcuts
Stop WebSocket & texthooker
Destroy tracker · clean Anki state"]:::shutdown
+ Loop -->|"quit signal"| Quit["will-quit"]:::shutdown
+
+ Quit --> T1["Tray · config watcher
global shortcuts"]:::shutdown
+ Quit --> T2["WebSocket · texthooker
mpv socket · OSD log"]:::shutdown
+ Quit --> T3["Window tracker
Yomitan parser"]:::shutdown
+ Quit --> T4["Immersion tracker
Jellyfin · Discord
Anki · AniList"]:::shutdown
- style Init fill:#363a4f,stroke:#494d64,color:#cad3f5
style Loop fill:#363a4f,stroke:#494d64,color:#cad3f5
```
diff --git a/docs/public/config.example.jsonc b/docs/public/config.example.jsonc
index f57972e..f7f4926 100644
--- a/docs/public/config.example.jsonc
+++ b/docs/public/config.example.jsonc
@@ -5,6 +5,7 @@
* Copy to $XDG_CONFIG_HOME/SubMiner/config.jsonc (or ~/.config/SubMiner/config.jsonc) and edit as needed.
*/
{
+
// ==========================================
// Overlay Auto-Start
// When overlay connects to mpv, automatically show overlay and hide mpv subtitles.
@@ -23,7 +24,7 @@
// Control whether browser opens automatically for 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.
// ==========================================
@@ -33,7 +34,7 @@
// ==========================================
"websocket": {
"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.
// ==========================================
@@ -42,7 +43,7 @@
// Set to debug for full runtime diagnostics.
// ==========================================
"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.
// ==========================================
@@ -64,7 +65,7 @@
"toggleSecondarySub": "CommandOrControl+Shift+V", // Toggle secondary sub setting.
"markAudioCard": "CommandOrControl+Shift+A", // Mark audio card 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.
// ==========================================
@@ -74,7 +75,7 @@
// This edit-mode shortcut is fixed and is not currently configurable.
// ==========================================
"invisibleOverlay": {
- "startupVisibility": "platform-default", // Startup visibility setting.
+ "startupVisibility": "platform-default" // Startup visibility setting.
}, // Startup behavior for the invisible interactive subtitle mining layer.
// ==========================================
@@ -94,7 +95,7 @@
"secondarySub": {
"secondarySubLanguages": [], // Secondary sub languages setting.
"autoLoadSecondarySub": false, // Auto load secondary sub setting. Values: true | false
- "defaultMode": "hover", // Default mode setting.
+ "defaultMode": "hover" // Default mode setting.
}, // Dual subtitle track options.
// ==========================================
@@ -105,7 +106,7 @@
"defaultMode": "auto", // Subsync default mode. Values: auto | manual
"alass_path": "", // Alass path setting.
"ffsubsync_path": "", // Ffsubsync path setting.
- "ffmpeg_path": "", // Ffmpeg path setting.
+ "ffmpeg_path": "" // Ffmpeg path setting.
}, // Subsync engine and executable paths.
// ==========================================
@@ -113,7 +114,7 @@
// Initial vertical subtitle position from the bottom.
// ==========================================
"subtitlePosition": {
- "yPercent": 10, // Y percent setting.
+ "yPercent": 10 // Y percent setting.
}, // Initial vertical subtitle position from the bottom.
// ==========================================
@@ -138,7 +139,7 @@
"N2": "#f5a97f", // N2 setting.
"N3": "#f9e2af", // N3 setting.
"N4": "#a6e3a1", // N4 setting.
- "N5": "#8aadf4", // N5 setting.
+ "N5": "#8aadf4" // N5 setting.
}, // Jlpt colors setting.
"frequencyDictionary": {
"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).
"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`.
- "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.
"secondary": {
"fontSize": 24, // Font size setting.
@@ -154,8 +161,8 @@
"backgroundColor": "transparent", // Background color setting.
"fontWeight": "normal", // Font weight 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.
- }, // Secondary 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.
}, // Primary and secondary subtitle styling.
// ==========================================
@@ -168,13 +175,15 @@
"enabled": false, // Enable AnkiConnect integration. Values: true | false
"url": "http://127.0.0.1:8765", // Url setting.
"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": {
"audio": "ExpressionAudio", // Audio setting.
"image": "Picture", // Image setting.
"sentence": "Sentence", // Sentence setting.
"miscInfo": "MiscInfo", // Misc info setting.
- "translation": "SelectionText", // Translation setting.
+ "translation": "SelectionText" // Translation setting.
}, // Fields setting.
"ai": {
"enabled": false, // Enabled setting. Values: true | false
@@ -183,7 +192,7 @@
"model": "openai/gpt-4o-mini", // Model setting.
"baseUrl": "https://openrouter.ai/api", // Base url 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.
"media": {
"generateAudio": true, // Generate audio setting. Values: true | false
@@ -196,7 +205,7 @@
"animatedCrf": 35, // Animated crf setting.
"audioPadding": 0.5, // Audio padding setting.
"fallbackDuration": 3, // Fallback duration setting.
- "maxMediaDuration": 30, // Max media duration setting.
+ "maxMediaDuration": 30 // Max media duration setting.
}, // Media setting.
"behavior": {
"overwriteAudio": true, // Overwrite audio setting. Values: true | false
@@ -204,7 +213,7 @@
"mediaInsertMode": "append", // Media insert mode setting.
"highlightWord": true, // Highlight word setting. Values: true | false
"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.
"nPlusOne": {
"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.
"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.
- "knownWord": "#a6da95", // Color used for legacy known-word highlights.
+ "knownWord": "#a6da95" // Color used for legacy known-word highlights.
}, // N plus one setting.
"metadata": {
- "pattern": "[SubMiner] %f (%t)", // Pattern setting.
+ "pattern": "[SubMiner] %f (%t)" // Pattern setting.
}, // Metadata setting.
"isLapis": {
"enabled": false, // Enabled setting. Values: true | false
- "sentenceCardModel": "Japanese sentences", // Sentence card model setting.
+ "sentenceCardModel": "Japanese sentences" // Sentence card model setting.
}, // Is lapis setting.
"isKiku": {
"enabled": false, // Enabled setting. Values: true | false
"fieldGrouping": "disabled", // Kiku duplicate-card field grouping mode. Values: auto | manual | disabled
- "deleteDuplicateInAuto": true, // Delete duplicate in auto setting. Values: true | false
- }, // Is kiku setting.
+ "deleteDuplicateInAuto": true // Delete duplicate in auto setting. Values: true | false
+ } // Is kiku setting.
}, // Automatic Anki updates and media generation options.
// ==========================================
@@ -236,7 +245,7 @@
"jimaku": {
"apiBaseUrl": "https://jimaku.cc", // Api base url setting.
"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.
// ==========================================
@@ -247,7 +256,10 @@
"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.
"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.
// ==========================================
@@ -256,7 +268,7 @@
// ==========================================
"anilist": {
"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.
// ==========================================
@@ -280,8 +292,16 @@
"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.
"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.
- "transcodeVideoCodec": "h264", // Preferred transcode video codec when direct play is unavailable.
+ "directPlayContainers": [
+ "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.
// ==========================================
@@ -292,7 +312,7 @@
"discordPresence": {
"enabled": false, // Enable optional Discord Rich Presence updates. Values: true | false
"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.
// ==========================================
@@ -314,7 +334,7 @@
"telemetryDays": 30, // Telemetry retention window in days.
"dailyRollupsDays": 365, // Daily rollup retention window in days.
"monthlyRollupsDays": 1825, // Monthly rollup retention window in days.
- "vacuumIntervalDays": 7, // Minimum days between VACUUM runs.
- }, // Retention setting.
- }, // Enable/disable immersion tracking.
+ "vacuumIntervalDays": 7 // Minimum days between VACUUM runs.
+ } // Retention setting.
+ } // Enable/disable immersion tracking.
}
diff --git a/docs/subagents/INDEX.md b/docs/subagents/INDEX.md
index 91b23c8..67ddb39 100644
--- a/docs/subagents/INDEX.md
+++ b/docs/subagents/INDEX.md
@@ -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-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-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` |
diff --git a/docs/subagents/agents/codex-commit-changes-20260223T050731Z-rer4.md b/docs/subagents/agents/codex-commit-changes-20260223T050731Z-rer4.md
new file mode 100644
index 0000000..ea88f9d
--- /dev/null
+++ b/docs/subagents/agents/codex-commit-changes-20260223T050731Z-rer4.md
@@ -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.
diff --git a/docs/subagents/agents/codex-plugin-start-overlay-gate-20260223T044347Z-k3n8.md b/docs/subagents/agents/codex-plugin-start-overlay-gate-20260223T044347Z-k3n8.md
new file mode 100644
index 0000000..b8a9c27
--- /dev/null
+++ b/docs/subagents/agents/codex-plugin-start-overlay-gate-20260223T044347Z-k3n8.md
@@ -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`.
diff --git a/docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md b/docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md
new file mode 100644
index 0000000..80b5d73
--- /dev/null
+++ b/docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md
@@ -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.
diff --git a/docs/subagents/collaboration.md b/docs/subagents/collaboration.md
index 45a4e89..774e0bf 100644
--- a/docs/subagents/collaboration.md
+++ b/docs/subagents/collaboration.md
@@ -148,6 +148,8 @@ Shared notes. Append-only.
## 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: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.
@@ -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: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: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.
diff --git a/initial-release.md b/initial-release.md
new file mode 100644
index 0000000..abd3756
--- /dev/null
+++ b/initial-release.md
@@ -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/-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.
diff --git a/plugin/subminer.lua b/plugin/subminer.lua
index 5303f7c..2955518 100644
--- a/plugin/subminer.lua
+++ b/plugin/subminer.lua
@@ -1526,7 +1526,8 @@ end
local function start_overlay(overrides)
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))
show_osd("SubMiner IPC not set up. Launch mpv with --input-ipc-server=/tmp/subminer-socket")
return
diff --git a/scripts/test-plugin-start-gate.lua b/scripts/test-plugin-start-gate.lua
new file mode 100644
index 0000000..8d67a38
--- /dev/null
+++ b/scripts/test-plugin-start-gate.lua
@@ -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")