From a07d5ecdb3e7a77fe6ae60e6bbd8add65102eb13 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 22 Feb 2026 21:08:25 -0800 Subject: [PATCH] fix(plugin): allow cold-start overlay launch without running process --- ...-and-draft-initial-release-cleanup-plan.md | 33 +++ ...start-gate-when-SubMiner-is-not-running.md | 60 +++++ config.example.jsonc | 82 ++++--- docs/architecture.md | 208 ++++++++++++------ docs/public/config.example.jsonc | 82 ++++--- docs/subagents/INDEX.md | 4 +- ...ex-commit-changes-20260223T050731Z-rer4.md | 26 +++ ...tart-overlay-gate-20260223T044347Z-k3n8.md | 30 +++ ...tial-release-plan-20260223T044059Z-p7k2.md | 35 +++ docs/subagents/collaboration.md | 4 + initial-release.md | 167 ++++++++++++++ plugin/subminer.lua | 3 +- scripts/test-plugin-start-gate.lua | 193 ++++++++++++++++ 13 files changed, 795 insertions(+), 132 deletions(-) create mode 100644 backlog/tasks/task-116 - Analyze-git-history-and-draft-initial-release-cleanup-plan.md create mode 100644 backlog/tasks/task-117 - Fix-mpv-plugin-overlay-start-gate-when-SubMiner-is-not-running.md create mode 100644 docs/subagents/agents/codex-commit-changes-20260223T050731Z-rer4.md create mode 100644 docs/subagents/agents/codex-plugin-start-overlay-gate-20260223T044347Z-k3n8.md create mode 100644 docs/subagents/agents/opencode-initial-release-plan-20260223T044059Z-p7k2.md create mode 100644 initial-release.md create mode 100644 scripts/test-plugin-start-gate.lua 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")