feat(browser): Vivaldi app-mode single window #6

Merged
slaid098 merged 6 commits from feat/browser/vivaldi-app-mode into master 2026-08-09 22:35:01 +03:00
Owner

Что сделано

  • webbrowser.open заменён на subprocess.Popen([vivaldi, "--app=<url>"]) — открывает видео в app-режиме (одно окно без вкладок и панели управления)
  • Перед каждым новым запуском вызывается taskkill /IM vivaldi.exe — старое app-окно закрывается, копления вкладок нет
  • Авто-detect пути к vivaldi.exe на Windows (LOCALAPPDATA, Program Files, Program Files (x86))
  • Fallback на webbrowser.open на не-Windows (Linux для dev/CI)
  • Новая настройка BROWSER_PATH в config.py и .env.template (пусто = авто-detect)
  • BrowserState хранит флаг открытого окна (PID не сохраняется)
  • Тесты test_browser.py переписаны: app-mode, taskkill перед новым окном, авто-detect, fallback, Vivaldi не найден
  • README: обновлён раздел про YouTube (app-режим, одно окно без вкладок)

Почему

Вкладки копились в браузере — ПК зависал от десятков открытых YouTube-вкладок. Мама слепая, не видит панель вкладок и не может их закрыть. App-режим Vivaldi открывает каждое видео в одном окне без интерфейса, а taskkill перед новым запуском гарантирует одно активное окно всегда.

Watch out

  • taskkill /IM vivaldi.exe закрывает весь Vivaldi (все окна, не только app-окна). Приемлемо: пользователь работает с одной вкладкой всегда, обычных окон Vivaldi не держит. Отличить app-окно от обычного по image name нельзя — app-режим запускает отдельный процесс, но image name общий.
  • BROWSER_PATH пусто по умолчанию — на Windows авто-detect ищет vivaldi.exe в типичных расположениях. Если Vivaldi установлен в нестандартное место — задаётся через env.
  • На Linux (dev/CI) — fallback на webbrowser.open без taskkill. В app-режиме открывается только на Windows.
  • Vivaldi не найден → лог error, тихий возврат (не падает). Озвучка ошибки делается на уровне assistant при необходимости.

Pending

  • Авто-установка Vivaldi при отсутствии — отдельный future-issue
  • Голосовая команда закрытия окна — не добавлена (требование пользователя)

Closes #2

Closes #2

## Что сделано - `webbrowser.open` заменён на `subprocess.Popen([vivaldi, "--app=<url>"])` — открывает видео в app-режиме (одно окно без вкладок и панели управления) - Перед каждым новым запуском вызывается `taskkill /IM vivaldi.exe` — старое app-окно закрывается, копления вкладок нет - Авто-detect пути к `vivaldi.exe` на Windows (LOCALAPPDATA, Program Files, Program Files (x86)) - Fallback на `webbrowser.open` на не-Windows (Linux для dev/CI) - Новая настройка `BROWSER_PATH` в `config.py` и `.env.template` (пусто = авто-detect) - `BrowserState` хранит флаг открытого окна (PID не сохраняется) - Тесты `test_browser.py` переписаны: app-mode, taskkill перед новым окном, авто-detect, fallback, Vivaldi не найден - README: обновлён раздел про YouTube (app-режим, одно окно без вкладок) ## Почему Вкладки копились в браузере — ПК зависал от десятков открытых YouTube-вкладок. Мама слепая, не видит панель вкладок и не может их закрыть. App-режим Vivaldi открывает каждое видео в одном окне без интерфейса, а `taskkill` перед новым запуском гарантирует одно активное окно всегда. ## Watch out - `taskkill /IM vivaldi.exe` закрывает **весь** Vivaldi (все окна, не только app-окна). Приемлемо: пользователь работает с одной вкладкой всегда, обычных окон Vivaldi не держит. Отличить app-окно от обычного по image name нельзя — app-режим запускает отдельный процесс, но image name общий. - `BROWSER_PATH` пусто по умолчанию — на Windows авто-detect ищет vivaldi.exe в типичных расположениях. Если Vivaldi установлен в нестандартное место — задаётся через env. - На Linux (dev/CI) — fallback на `webbrowser.open` без taskkill. В app-режиме открывается только на Windows. - Vivaldi не найден → лог error, тихий возврат (не падает). Озвучка ошибки делается на уровне assistant при необходимости. ## Pending - Авто-установка Vivaldi при отсутствии — отдельный future-issue - Голосовая команда закрытия окна — не добавлена (требование пользователя) Closes #2 Closes #2
docs(browser): update README and .env.template
Some checks failed
CI / typecheck (pull_request) Successful in 1m30s
CI / lint (pull_request) Failing after 1m31s
CI / complexity (pull_request) Successful in 41s
CI / test (3.13) (pull_request) Successful in 4m19s
CI / test (3.12) (pull_request) Successful in 4m23s
020a6ab946
fix(ci): ruff format test_browser assertion
All checks were successful
CI / lint (pull_request) Successful in 1m25s
CI / typecheck (pull_request) Successful in 1m27s
CI / complexity (pull_request) Successful in 32s
CI / test (3.12) (pull_request) Successful in 4m11s
CI / test (3.13) (pull_request) Successful in 4m11s
3d52f1246d
Author
Owner

Code Review Summary

Summary

PR реализует app-режим Vivaldi для одного окна без вкладок — архитектурно чисто (Protocol-совместимый BrowserState, fallback на не-Windows, моки subprocess в тестах). Но найдена критическая несогласованность: настройка BROWSER_PATH добавлена в config.py, .env.template и README, но browser.py её не читает — dead config, нарушение контракта PR.

Critical (must fix before merge)

  • src/voice_assistant/services/browser.py:88 [contract] BROWSER_PATH не используется — dead config.
    PR body заявляет: «Новая настройка BROWSER_PATH в config.py и .env.template (пусто = авто-detect)». README: «Путь к Vivaldi (пусто = авто-detect на Windows)». .env.template: «Путь к Vivaldi (если пусто — авто-detect на Windows)». Но browser.py не импортирует settings и не читает settings.browser_path — вызывается только _detect_vivaldi_path(). Пользователь задаёт BROWSER_PATH=C:\My\Vivaldi\vivaldi.exe в нестандартном расположении → config.py загружает значение → browser.py игнорирует → авто-detect не находит (4 типичных пути) → лог «Vivaldi не найден — задайте BROWSER_PATH» (но задание ничего не даёт).
    Fix: в open_url проверить settings.browser_path перед авто-detect:
    from voice_assistant.config import settings
    ...
    browser_path = settings.browser_path or _detect_vivaldi_path()
    
    Добавить тест test_open_url_uses_browser_path_from_config — мок settings.browser_path="C:\\Custom\\vivaldi.exe", assert Popen вызван с этим путём, _detect_vivaldi_path не вызывается.

Cross-file impact: missing paired update

PR #6 меняет writer config.py:55,211 (добавляет browser_path: str поле + BROWSER_PATH env var).
browser.py:open_url — предполагаемый reader settings.browser_path — НЕ обновлён:

  • config.py:55 пишет browser_path в Settings (writer)
  • browser.py:88 читает только _detect_vivaldi_path() (reader не подключён)
  • config.py меняет контракт (новое поле) → browser.py должен его потреблять → иначе dead config

ТЗ на fix:

  • Что: open_url должен использовать settings.browser_path (если задан) с fallback на _detect_vivaldi_path()
  • Где: src/voice_assistant/services/browser.py:88
  • Контракт: browser_path = settings.browser_path or _detect_vivaldi_path(); если оба None → лог error
  • Тесты: tests/test_browser.py — добавить test_open_url_uses_browser_path_from_config

Добавьте fix в этот PR. ~3 строки в browser.py + ~15 строк тестов.

Warnings (should fix)

  • src/voice_assistant/services/browser.py:84 [error-handling] except Exception — слишком broad для fallback. webbrowser.open бросает webbrowser.Error (или OSError). Сузить до except (webbrowser.Error, OSError) as ex.
    Fix: except (webbrowser.Error, OSError) as ex:

  • src/voice_assistant/services/browser.py:102 [robustness] subprocess.Popen([browser_path, f"--app={url}"]) без stdout=DEVNULL, stderr=DEVNULL — Vivaldi может писать шум в консоль ассистента (chromium logs). Для end-user приложения это шум.
    Fix: subprocess.Popen([browser_path, f"--app={url}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

  • PR body vs code [docs] PR body: «Авто-detect пути к vivaldi.exe на Windows (LOCALAPPDATA, Program Files, Program Files (x86))» — 3 пути. Код (browser.py:42-58) проверяет 4 пути: LOCALAPPDATA, USERPROFILE, Program Files, Program Files (x86). Расхождение body↔code — обновить body или README.
    Fix: уточнить в PR body «4 типичных расположения».

Positives

  • App-mode через --app=<url> — корректное решение для одного окна без вкладок (Chrome/Vivaldi app mode).
  • taskkill перед новым запуском — гарантирует одно активное окно, решает исходную проблему #2 (копление вкладок).
  • Fallback на webbrowser.open для Linux/dev/CI — тестируемость сохранена.
  • # noqa: S603 обоснован: путь из _detect_vivaldi_path проверен os.path.isfile. # noqa: S607 для taskkill — системная утилита Windows, ок.
  • PID не сохраняется (только _has_open_window флаг) — соответствует требованию, упрощает state.
  • BROWSER_PROFILE не добавлен — корректно (uBlock не нужен, Vivaldi блокирует рекламу по умолчанию).
  • Тесты мокают subprocess.Popen/subprocess.run — реальный Vivaldi не запускается, CI-safe.
  • Покрытие тестами: app-mode, taskkill перед новым окном, Vivaldi не найден, fallback webbrowser, webbrowser error, авто-detect (найден/не найден/non-Windows) — 8 тестов на новое поведение.
  • platform.system() вместо sys.platform — корректнее для detect (работает и в CI-контейнерах).
  • Docstrings на русском для публичного API — соответствует AGENTS.md (русский язык проекта).
  • PR body: ## Что сделано / ## Почему / ## Watch out / ## Pending — все заполнены осмысленно, Watch out честно описывает ограничение taskkill (закрывает весь Vivaldi).

Verdict: REQUEST_CHANGES

## Code Review Summary ### Summary PR реализует app-режим Vivaldi для одного окна без вкладок — архитектурно чисто (Protocol-совместимый `BrowserState`, fallback на не-Windows, моки subprocess в тестах). Но найдена критическая несогласованность: настройка `BROWSER_PATH` добавлена в `config.py`, `.env.template` и README, но `browser.py` её **не читает** — dead config, нарушение контракта PR. ### Critical (must fix before merge) - **src/voice_assistant/services/browser.py:88** [contract] `BROWSER_PATH` не используется — dead config. PR body заявляет: «Новая настройка `BROWSER_PATH` в `config.py` и `.env.template` (пусто = авто-detect)». README: «Путь к Vivaldi (пусто = авто-detect на Windows)». `.env.template`: «Путь к Vivaldi (если пусто — авто-detect на Windows)». Но `browser.py` не импортирует `settings` и не читает `settings.browser_path` — вызывается только `_detect_vivaldi_path()`. Пользователь задаёт `BROWSER_PATH=C:\My\Vivaldi\vivaldi.exe` в нестандартном расположении → `config.py` загружает значение → `browser.py` игнорирует → авто-detect не находит (4 типичных пути) → лог «Vivaldi не найден — задайте BROWSER_PATH» (но задание ничего не даёт). Fix: в `open_url` проверить `settings.browser_path` перед авто-detect: ```python from voice_assistant.config import settings ... browser_path = settings.browser_path or _detect_vivaldi_path() ``` Добавить тест `test_open_url_uses_browser_path_from_config` — мок `settings.browser_path="C:\\Custom\\vivaldi.exe"`, assert `Popen` вызван с этим путём, `_detect_vivaldi_path` не вызывается. ## Cross-file impact: missing paired update PR #6 меняет writer `config.py:55,211` (добавляет `browser_path: str` поле + `BROWSER_PATH` env var). `browser.py:open_url` — предполагаемый reader `settings.browser_path` — НЕ обновлён: - `config.py:55` пишет `browser_path` в `Settings` (writer) - `browser.py:88` читает только `_detect_vivaldi_path()` (reader не подключён) - `config.py` меняет контракт (новое поле) → `browser.py` должен его потреблять → иначе dead config ТЗ на fix: - Что: `open_url` должен использовать `settings.browser_path` (если задан) с fallback на `_detect_vivaldi_path()` - Где: `src/voice_assistant/services/browser.py:88` - Контракт: `browser_path = settings.browser_path or _detect_vivaldi_path()`; если оба None → лог error - Тесты: `tests/test_browser.py` — добавить `test_open_url_uses_browser_path_from_config` Добавьте fix в этот PR. ~3 строки в browser.py + ~15 строк тестов. ### Warnings (should fix) - **src/voice_assistant/services/browser.py:84** [error-handling] `except Exception` — слишком broad для fallback. `webbrowser.open` бросает `webbrowser.Error` (или `OSError`). Сузить до `except (webbrowser.Error, OSError) as ex`. Fix: `except (webbrowser.Error, OSError) as ex:` - **src/voice_assistant/services/browser.py:102** [robustness] `subprocess.Popen([browser_path, f"--app={url}"])` без `stdout=DEVNULL, stderr=DEVNULL` — Vivaldi может писать шум в консоль ассистента (chromium logs). Для end-user приложения это шум. Fix: `subprocess.Popen([browser_path, f"--app={url}"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)` - **PR body vs code** [docs] PR body: «Авто-detect пути к `vivaldi.exe` на Windows (LOCALAPPDATA, Program Files, Program Files (x86))» — 3 пути. Код (`browser.py:42-58`) проверяет 4 пути: LOCALAPPDATA, USERPROFILE, Program Files, Program Files (x86). Расхождение body↔code — обновить body или README. Fix: уточнить в PR body «4 типичных расположения». ### Positives - App-mode через `--app=<url>` — корректное решение для одного окна без вкладок (Chrome/Vivaldi app mode). - `taskkill` перед новым запуском — гарантирует одно активное окно, решает исходную проблему #2 (копление вкладок). - Fallback на `webbrowser.open` для Linux/dev/CI — тестируемость сохранена. - `# noqa: S603` обоснован: путь из `_detect_vivaldi_path` проверен `os.path.isfile`. `# noqa: S607` для `taskkill` — системная утилита Windows, ок. - PID не сохраняется (только `_has_open_window` флаг) — соответствует требованию, упрощает state. - `BROWSER_PROFILE` не добавлен — корректно (uBlock не нужен, Vivaldi блокирует рекламу по умолчанию). - Тесты мокают `subprocess.Popen`/`subprocess.run` — реальный Vivaldi не запускается, CI-safe. - Покрытие тестами: app-mode, taskkill перед новым окном, Vivaldi не найден, fallback webbrowser, webbrowser error, авто-detect (найден/не найден/non-Windows) — 8 тестов на новое поведение. - `platform.system()` вместо `sys.platform` — корректнее для detect (работает и в CI-контейнерах). - Docstrings на русском для публичного API — соответствует AGENTS.md (русский язык проекта). - PR body: `## Что сделано` / `## Почему` / `## Watch out` / `## Pending` — все заполнены осмысленно, `Watch out` честно описывает ограничение taskkill (закрывает весь Vivaldi). ### Verdict: REQUEST_CHANGES
fix(browser): honor BROWSER_PATH setting before auto-detect
Some checks failed
CI / lint (pull_request) Failing after 1m38s
CI / typecheck (pull_request) Successful in 1m38s
CI / complexity (pull_request) Has been cancelled
CI / test (3.12) (pull_request) Has been cancelled
CI / test (3.13) (pull_request) Has been cancelled
baf64c864f
style(browser): ruff format test_browser.py
All checks were successful
CI / lint (pull_request) Successful in 1m25s
CI / typecheck (pull_request) Successful in 1m24s
CI / complexity (pull_request) Successful in 31s
CI / test (3.13) (pull_request) Successful in 3m56s
CI / test (3.12) (pull_request) Successful in 4m6s
091bd771fb
Author
Owner

Code Review Summary

Re-review после фиксов (baf64c8, 091bd77). Все 4 замечания из предыдущего review проверены — 3 исправлены, 1 (расхождение body/кода) осознанно проигнорировано как не-блокер.

Проверка замечаний

  1. BROWSER_PATH не читался исправлено. browser.py:89 теперь settings.browser_path or _detect_vivaldi_path(): если путь задан в settings, авто-detect не вызывается. Тест test_browser_path_from_settings верифицирует это через _fail_if_called sentinel — авто-detect падает с AssertionError, если вызван, и тест подтверждает detect_called["yes"] is False.

  2. except Exception слишком broad исправлено. browser.py:85 сужен до except (webbrowser.Error, OSError) (fallback на не-Windows), browser.py:110except OSError (запуск Vivaldi). Тесты test_open_url_non_windows_webbrowser_error и test_open_url_non_windows_oserror_fallback покрывают обе ветки.

  3. subprocess.Popen без DEVNULL исправлено. browser.py:105-106 добавлены stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL. Тесты верифицируют через assert_called_once_with(..., stdout=DEVNULL, stderr=DEVNULL).

  4. Расхождение body/кода 3 vs 4 пути — осознанно проигнорировано (не блокер), ок.

Регрессии и новые проблемы

  • Удаление _current_url_has_open_window: callers (handlers.py, youtube_flow.py) используют только публичные функции open_browser_url и get_current_title — не затронуты. Проверено через rg.
  • Тесты: 15/15 passed (uv run pytest tests/test_browser.py).
  • Ruff: All checks passed (browser.py, test_browser.py, config.py).
  • Mypy: Success, no issues (browser.py, config.py).
  • Coverage browser.py: 90% (5 miss из 63 stmts — строки 23, 46, 110-111, 138 — Windows-only импорт и error-логи, ожидаемо на Linux CI).

Positives

  • _detect_vivaldi_path() хорошо структурирован: 4 кандидата (LOCALAPPDATA, USERPROFILE-derived, Program Files, Program Files x86), проверка через os.path.isfile, ранний return на не-Windows.
  • Тесты используют dataclasses.replace(settings, browser_path=...) для изоляции — чистый подход без мутации глобального settings.
  • noqa: S603/S607 аннотированы осознанно (subprocess с динамическим путём / taskkill без полного пути).
  • Docstrings на русском, соответствуют стилю проекта.

Suggestions (info, not blocking)

  • browser.py:94 if self._has_open_window — taskkill вызывается только если ранее уже открывали окно. Если пользователь вручную закрыл Vivaldi, _has_open_window остаётся True → taskkill будет вызван впустую при следующем open_url. Это безвредно (taskkill с check=False), но можно отметить как известный trade-off. Не блокер.

Verdict: APPROVE

## Code Review Summary Re-review после фиксов (`baf64c8`, `091bd77`). Все 4 замечания из предыдущего review проверены — 3 исправлены, 1 (расхождение body/кода) осознанно проигнорировано как не-блокер. ### Проверка замечаний 1. **BROWSER_PATH не читался** — ✅ исправлено. `browser.py:89` теперь `settings.browser_path or _detect_vivaldi_path()`: если путь задан в settings, авто-detect не вызывается. Тест `test_browser_path_from_settings` верифицирует это через `_fail_if_called` sentinel — авто-detect падает с AssertionError, если вызван, и тест подтверждает `detect_called["yes"] is False`. 2. **`except Exception` слишком broad** — ✅ исправлено. `browser.py:85` сужен до `except (webbrowser.Error, OSError)` (fallback на не-Windows), `browser.py:110` — `except OSError` (запуск Vivaldi). Тесты `test_open_url_non_windows_webbrowser_error` и `test_open_url_non_windows_oserror_fallback` покрывают обе ветки. 3. **`subprocess.Popen` без DEVNULL** — ✅ исправлено. `browser.py:105-106` добавлены `stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL`. Тесты верифицируют через `assert_called_once_with(..., stdout=DEVNULL, stderr=DEVNULL)`. 4. **Расхождение body/кода 3 vs 4 пути** — осознанно проигнорировано (не блокер), ок. ### Регрессии и новые проблемы - **Удаление `_current_url`** → `_has_open_window`: callers (`handlers.py`, `youtube_flow.py`) используют только публичные функции `open_browser_url` и `get_current_title` — не затронуты. Проверено через `rg`. - **Тесты**: 15/15 passed (`uv run pytest tests/test_browser.py`). - **Ruff**: All checks passed (browser.py, test_browser.py, config.py). - **Mypy**: Success, no issues (browser.py, config.py). - **Coverage browser.py**: 90% (5 miss из 63 stmts — строки 23, 46, 110-111, 138 — Windows-only импорт и error-логи, ожидаемо на Linux CI). ### Positives - `_detect_vivaldi_path()` хорошо структурирован: 4 кандидата (LOCALAPPDATA, USERPROFILE-derived, Program Files, Program Files x86), проверка через `os.path.isfile`, ранний return на не-Windows. - Тесты используют `dataclasses.replace(settings, browser_path=...)` для изоляции — чистый подход без мутации глобального settings. - `noqa: S603`/`S607` аннотированы осознанно (subprocess с динамическим путём / taskkill без полного пути). - Docstrings на русском, соответствуют стилю проекта. ### Suggestions (info, not blocking) - **browser.py:94** `if self._has_open_window` — taskkill вызывается только если ранее уже открывали окно. Если пользователь вручную закрыл Vivaldi, `_has_open_window` остаётся True → taskkill будет вызван впустую при следующем open_url. Это безвредно (taskkill с `check=False`), но можно отметить как известный trade-off. Не блокер. ### Verdict: APPROVE
slaid098 merged commit 2cde9b7f7e into master 2026-08-09 22:35:01 +03:00
slaid098 deleted branch feat/browser/vivaldi-app-mode 2026-08-09 22:35:01 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/voice_assistant!6
No description provided.