fix(ui): broaden page gate + debug logging + version bump (#30)

## Что сделано
- Расширен gate `isOpencodePage()` в `src/insert.ts`: теперь проверяет
любой из 6 селекторов (`prompt-input`, `prompt-input-v2`,
`session-prompt-dock`, `session-composer`, `session-new-composer`,
`question-custom-input`) вместо одного `prompt-input`. Это активирует
скрипт раньше на v1.18.x, где композер может монтироваться lazy.
- Добавлено отладочное логирование с префиксом `[ocvd]` в `src/index.ts`
(init, gate retry) и `src/ui.ts` (composer found/not found, mic button
injected). Пользователь может видеть в DevTools Console где скрипт
останавливается.
- Бамп `@version` до `1.0.1` в `package.json` и `vite.config.ts` —
разблокирует автообновление userscript-менеджеров (после PR #29 версия
осталась `1.0.0`, автообновление не сработает).

## Почему
PR #29 добавил корректные селекторы для v1.18.x, но пользователи не
получили обновление из-за не-бампнутой `@version`. Также gate
`isOpencodePage()` проверял только `prompt-input`, которого может не
быть в DOM на ранних этапах lazy-mount. Добавление логирования поможет
диагностировать runtime-проблемы в DevTools.

Closes #28

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-07-25 02:03:39 +03:00 committed by GitHub
parent af5bc63bcc
commit 36169b4b08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 211 additions and 3 deletions

View file

@ -0,0 +1,95 @@
# ADR 0002: Broaden page detection gate + debug logging + version bump for v1.18.x
- **Date**: 2026-07-24
- **PR**: #30
- **Parent PR**: #29
- **Issue**: #28
## Статус
Accepted.
## Контекст
PR #29 исправил селекторы композера (`prompt-input-v2`, `session-prompt-dock`) для OpenCode web v1.18.x — live-браузер на opencode.slaid098.dev v1.18.3 подтвердил их корректность. Однако три пробела оставались:
1. **Gate `isOpencodePage()` проверял один селектор.** В `src/insert.ts` функция возвращала `document.querySelector('[data-component="prompt-input"]') !== null`. На v1.18.x `prompt-input` монтируется **lazy** — появляется асинхронно после загрузки сообщений сессии, внутри `session-prompt-dock`. В момент userscript `run-at: document-idle` ни `prompt-input`, ни `prompt-input-v2`, ни `session-prompt-dock` ещё не в DOM. `isOpencodePage()` возвращал `false`, `init()` уходил в `setTimeout(init, 1500)` и крутился в retry, задерживая инициализацию UI на 1.5s+ (а при медленной сети — дольше). Пользователь не видел, что скрипт вообще пытается.
2. **Нет отладочного логирования.** Все точки отказа были тихими: `isOpencodePage() === false`, `findComposer() === null`, успешный inject кнопки 🎤 — ни одно не писало в DevTools Console. Диагностика «скрипт не работает у пользователя» превращалась в гадание: загрузился ли userscript вообще? дошёл ли до gate? прошёл gate? нашёл композер? внедрил кнопку? Без логирования поддержке не за что зацепиться.
3. **`@version` осталась `1.0.0` после PR #29.** PR #29 менял код селекторов, но не бампал версию. Userscript-менеджеры (Violentmonkey, Tampermonkey) сравнивают `@version` из `.meta.js` (генерируется из `vite.config.ts` `userscript.version` через vite-plugin-monkey) — равные версии = нет автообновления. Пользователи оставались на сломанном билде PR #28, хотя исправление уже в репо.
Дополнительные ограничения:
- `oldInterfaceSunset = 2026-09-14` (`packages/app/src/context/settings.tsx:62`) — legacy UI окончательно уходит; селекторы `session-composer` / `session-new-composer` в gate будут мёртвым кодом после этой даты (общая pending-задача с ADR 0001).
- `run-at: document-idle` в `vite.config.ts` — userscript запускается после парсинга DOM, но до полного hydration SPA OpenCode. Lazy-mount композера — норма для SPA, не баг OpenCode.
- `match: ["*://*/*"]` — скрипт грузится на всех сайтах; gate должен отсеивать не-OpenCode страницы без побочных эффектов.
## Решение
**1. Расширить gate до 6 селекторов (любой = страница OpenCode с композером).** Добавить массив `PAGE_DETECT_SELECTORS` в `src/insert.ts`, `isOpencodePage()` проверяет через `Array.some()`:
```ts
const PAGE_DETECT_SELECTORS = [
'[data-component="prompt-input"]',
'[data-component="prompt-input-v2"]',
'[data-component="session-prompt-dock"]',
'[data-component="session-composer"]',
'[data-component="session-new-composer"]',
'[data-slot="question-custom-input"]',
];
export function isOpencodePage(): boolean {
return PAGE_DETECT_SELECTORS.some((s) => document.querySelector(s) !== null);
}
```
Множественные селекторы ловят любой ранний маркер страницы OpenCode — `session-prompt-dock` монтируется раньше `prompt-input` в lifecycle v1.18.x, что сокращает задержку инициализации. `session-composer` / `session-new-composer` сохранены как fallback для pre-1.18 (до `oldInterfaceSunset`).
**2. Добавить `console.log` с префиксом `[ocvd]`** в ключевых точках lifecycle:
- `src/index.ts init()`: `[ocvd] OpenCode page not detected, retrying in 1.5s...` (retry) и `[ocvd] OpenCode page detected, setting up UI` (успех)
- `src/ui.ts findComposer()`: `[ocvd] Composer found via ${selector}` (какой селектор сработал) и `[ocvd] No composer found in DOM` (провал)
- `src/ui.ts injectIntoComposer()`: `[ocvd] Mic button injected into composer` (успех inject)
Единый префикс `[ocvd]` = grep-маркер в DevTools Console. Пять точек покрывают весь путь: gate retry → gate success → composer search → composer found/miss → button inject.
**3. Бамп `@version` до `1.0.1` в обоих source-of-truth:**
- `package.json` `"version": "1.0.1"` (npm/semver)
- `vite.config.ts` `userscript.version: "1.0.1"` (vite-plugin-monkey → `.meta.js` → userscript-менеджеры)
Синхронный бамп обоих файлов обязателен: vite-plugin-monkey берёт `@version` только из `vite.config.ts`, `package.json` на userscript-менеджеры не влияет.
## Альтернативы
### 1. Ждать lazy-mount `prompt-input` без расширения gate
- **Плюс**: минимальная правка, gate остаётся на одном селекторе.
- **Минус**: на v1.18.x `prompt-input` монтируется последним в цепочке (`session-prompt-dock``prompt-input-v2``prompt-input` contenteditable). Gate на одном `prompt-input` добавляет +1.5s+ задержки инициализации на каждый retry-цикл, на медленной сети — кратно больше. Пользователь видит пустой композер без кнопки 🎤 дольше необходимого.
### 2. MutationObserver для gate вместо polling
- **Плюс**: реактивность — `isOpencodePage()` срабатывает в момент появления любого селектора, без retry-цикла 1500ms.
- **Минус**: сложнее. Требует переделки `init()` с polling на observer, обработки disconnect, аккуратной работы с re-mount (SPA-навигация между сессиями). Текущий polling 1500ms достаточно лёгкий и понятный; переход на observer — отдельная задача с тестами. Для PR #30 (hotfix) избыточно.
### 3. Single source of truth для `@version` через `process.env.npm_package_version`
- **Плюс**: бамп только `package.json`, `vite.config.ts` читает версию через `process.env.npm_package_version` (vite подставляет автоматически). Исключает рассинхрон.
- **Минус**: требует тестов на сборку (verify, что `userscript.version` в `.meta.js` реально подставился из env), возможны edge-cases с vite-plugin-monkey (плагин может требовать строковый литерал в schema). Отложено — не blocking для PR #30, кандидат на отдельный PR с тестами.
### 4. Вынести логирование под флаг `GM_getValue("debug")`
- **Плюс**: production-билд чистый, логи только для диагностики.
- **Минус**: лишний `GM_getValue`-вызов на каждый лог, плюс пользователь не сможет включить логи без знания меню Violentmonkey. Для PR #30 (диагностика после ряда жалоб) важнее «логи видны всегда» — флаг откладывается до момента, когда логи станут шумом.
## Последствия
- Gate `isOpencodePage()` проходит на любой странице, где присутствует хотя бы один из 6 селекторов — задержка инициализации на v1.18.x сокращается (ранний маркер `session-prompt-dock` ловится раньше `prompt-input`).
- DevTools Console содержит трассу `[ocvd]` от gate до inject — диагностика «скрипт не работает» сводится к grep `[ocvd]` в Console.
- `@version` 1.0.1 в `.meta.js` разблокирует автообновление userscript-менеджеров — пользователи получают исправление PR #29 + PR #30 одним обновлением.
- Логирование `[ocvd]` остаётся в production-билде (vite-plugin-monkey не дропает `console.log`); если станет шумом — убрать или вынести под флаг.
- `PAGE_DETECT_SELECTORS` и `COMPOSER_SELECTORS` (ADR 0001) частично перекрываются, но служат разным целям: gate (`some()`, порядок не важен) vs composer-find (`for...of`, порядок = приоритет). Не объединять.
- После `oldInterfaceSunset` (14.09.2026) legacy-селекторы `session-composer` / `session-new-composer` в `PAGE_DETECT_SELECTORS` станут мёртвым кодом — удалить вместе с cleanup из ADR 0001.
## Источники
- `src/insert.ts` (текущий): `PAGE_DETECT_SELECTORS`, `isOpencodePage()`
- `src/index.ts` (текущий): `init()`, логирование gate
- `src/ui.ts` (текущий): `findComposer()`, `injectIntoComposer()`, логирование composer
- `vite.config.ts` (текущий): `userscript.version`, `run-at: document-idle`
- Предыдущий ADR: `docs/decisions/0001-pr-29-v1-18-composer-selectors-fallback-order.md` (порядок `COMPOSER_SELECTORS`, `oldInterfaceSunset`)
- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` (lazy-mount композера на v1.18.x)

View file

@ -0,0 +1,100 @@
---
pr_number: 30
branch: fix/ui/v1-18-runtime-gate-debug
parent_pr: 29
title: broaden page gate + debug logging + version bump for v1.18.x
status: open
created: 2026-07-24
---
# Handoff — PR #30: v1.18.x runtime gate + debug logging + version bump
## Контекст
PR #29 (`fix/ui): support OpenCode 1.18.x composer selectors`) добавил корректные селекторы `prompt-input-v2` и `session-prompt-dock` в массив `COMPOSER_SELECTORS` — live-браузер на opencode.slaid098.dev v1.18.3 подтвердил их валидность. Однако PR #29 оставил три пробела, из-за которых скрипт всё ещё не запускался у реальных пользователей:
1. **`@version` не бампнута.** В `package.json` и `vite.config.ts` осталась `1.0.0` (та же, что у PR #29). Userscript-менеджеры (Violentmonkey/Tampermonkey) сравнивают `@version` из `.meta.js` для автообновления — совпадение версий = нет обновления = пользователи остаются на сломанном билде PR #28.
2. **Gate `isOpencodePage()` узкий.** В `src/insert.ts` функция проверяла только `[data-component="prompt-input"]`. На v1.18.x композер монтируется lazy (после загрузки сообщений сессии), а `prompt-input` в `PAGE_DETECT_SELECTORS` стоит первым — но в момент `document-idle` run-at ни один из 6 селекторов может ещё не быть в DOM. Скрипт уходил в retry каждые 1500ms и пользователь не понимал, запустился ли он вообще.
3. **Нет отладочного логирования.** Все точки отказа (`isOpencodePage() === false`, `findComposer() === null`, успешный inject) были тихими — в DevTools Console ничего не появлялось, диагностика «скрипт не работает» превращалась в гадание.
Issue: #28 (родительский). PR #30 — продолжение без новой issue.
## Что сделано
Три коммита на ветке `fix/ui/v1-18-runtime-gate-debug`:
- **`d5f80e7` fix(ui): broaden page detection gate for v1.18.x** — `src/insert.ts`: добавлен массив `PAGE_DETECT_SELECTORS` из 6 селекторов, `isOpencodePage()` теперь возвращает `true` при совпадении любого (через `.some()`). Расширение с 1 селектора (`prompt-input`) до 6 (`prompt-input`, `prompt-input-v2`, `session-prompt-dock`, `session-composer`, `session-new-composer`, `question-custom-input`).
- **`6982bfb` feat(ui): add debug logging for init and composer detection** — `src/index.ts` (2 строки в `init()`) + `src/ui.ts` (3 строки в `findComposer()` / `injectIntoComposer()`): `console.log` с унифицированным префиксом `[ocvd]` в каждой ключевой точке lifecycle.
- **`af3edc7` chore: bump version to 1.0.1** — `package.json` и `vite.config.ts`: `1.0.0``1.0.1` в обоих файлах (синхронно).
## Почему
- **`@version` не бампнута в PR #29.** Userscript-менеджеры не подтягивают изменения, если `@version` в `.meta.js` не выросла. PR #29 менял код селекторов, но не трогал версию — пользователи получали «обновление недоступно», хотя код был исправлен. PR #30 бампает версию в обоих source-of-truth (`package.json` для npm, `vite.config.ts` userscript.version для vite-plugin-monkey).
- **Gate проверял только `prompt-input`.** На v1.18.x `prompt-input` монтируется lazy внутри `session-prompt-dock` после hydration сессии. В момент `document-idle` run-at элемента ещё нет, `isOpencodePage()` возвращает `false`, `init()` уходит в `setTimeout(init, 1500)` и крутится до тех пор, пока композер не появится. С 6 селекторами `some()` ловит любой ранний маркер страницы OpenCode (`session-prompt-dock` монтируется раньше `prompt-input`).
- **Нет логирования для диагностики.** Без `console.log` пользователь не мог отличить «скрипт не загрузился» от «скрипт загрузился, но gate не прошёл» от «скрипт загрузился, gate прошёл, но композер не найден». Префикс `[ocvd]` даёт единый grep-маркер в DevTools Console.
## Pending
- После merge проверить live на opencode.slaid098.dev v1.18.3: открыть DevTools Console, обновить страницу с установленным userscript 1.0.1 — должны появиться `[ocvd] OpenCode page detected, setting up UI``[ocvd] Composer found via [data-component="prompt-input-v2"]``[ocvd] Mic button injected into composer`. Если вместо `detected` крутится `not detected, retrying in 1.5s...` — gate недостаточен, расширять `PAGE_DETECT_SELECTORS` дальше.
- Подтвердить, что userscript-менеджеры подтянули автообновление: сравнить `@version` в `about:` для установленного скрипта с `1.0.1`. Если менеджер не видит обновление — проверить, что `updateURL` в `vite.config.ts` указывает на свежий `.meta.js` в ветке `dist/` (или `main` после merge).
- После `oldInterfaceSunset` (2026-09-14) legacy-селекторы `session-composer` / `session-new-composer` в `PAGE_DETECT_SELECTORS` и `COMPOSER_SELECTORS` станут мёртвым кодом — удалить отдельным PR (общая задача с PR #29 pending).
- Логирование `[ocvd]` остаётся в production: если после стабилизации появится шум в Console пользователей — вынести под флаг `GM_getValue("debug")` или убрать полностью.
## Watch out
- **vite-plugin-monkey берёт `@version` из `vite.config.ts` `userscript.version`, НЕ из `package.json`.** `package.json.version` нужен только для npm/semver-инструментов. Рассинхрон = бамп одного файла → userscript-менеджеры не видят обновления, хотя npm видит. PR #30 бампает оба синхронно, но в будущем это ловушка — кандидат на single source of truth через `process.env.npm_package_version` (см. ADR 0002, альтернатива 3).
- **Логирование `[ocvd]` останется в production-билде.** `vite-plugin-monkey` не вырезает `console.log` по умолчанию. Все 5 точек (`index.ts:177,181`, `ui.ts:254,258,306`) попадут в `.user.js`. Не забывать убирать если будет шум, либо включать `terser` drop_console.
- **Порядок `PAGE_DETECT_SELECTORS` не имеет семантического значения** (в отличие от `COMPOSER_SELECTORS`, где порядок = приоритет): `some()` возвращает `true` по первому совпадению, gate только решает «страница OpenCode или нет». Не путать с `COMPOSER_SELECTORS` из ADR 0001, где порядок критичен.
- **`src/insert.ts` `PROMPT_INPUT_SELECTOR` (одиночный) не удалён** — он используется `insertIntoContenteditable()` для фактической вставки текста, не для gate. Не объединять с `PAGE_DETECT_SELECTORS`.
- **Retry `setTimeout(init, 1500)` не имеет backoff/лимита** — если страница никогда не станет OpenCode (false match на стороннем сайте), скрипт крутится бесконечно. На текущем `match: ["*://*/*"]` это потенциальная утечка таймера; вне scope PR #30, но отметить на будущее.
## Изменения
### `src/insert.ts`
- Добавлен массив `PAGE_DETECT_SELECTORS` (6 селекторов):
```ts
const PAGE_DETECT_SELECTORS = [
'[data-component="prompt-input"]',
'[data-component="prompt-input-v2"]',
'[data-component="session-prompt-dock"]',
'[data-component="session-composer"]',
'[data-component="session-new-composer"]',
'[data-slot="question-custom-input"]',
];
```
- `isOpencodePage()` изменён с `document.querySelector(PROMPT_INPUT_SELECTOR) !== null` на `PAGE_DETECT_SELECTORS.some((s) => document.querySelector(s) !== null)`.
- `PROMPT_INPUT_SELECTOR`, `SUBMIT_SELECTOR`, `QUESTION_INPUT_SELECTOR` — без изменений (используются `insertIntoContenteditable` / `submitPrompt` / `isQuestionPromptOpen`).
### `src/index.ts`
- `init()`: добавлены 2 `console.log``[ocvd] OpenCode page not detected, retrying in 1.5s...` (ветка retry) и `[ocvd] OpenCode page detected, setting up UI` (ветка успеха).
### `src/ui.ts`
- `findComposer()`: добавлен `console.log(\`[ocvd] Composer found via ${selector}\`)` при совпадении и `[ocvd] No composer found in DOM` при провале.
- `injectIntoComposer()`: добавлен `[ocvd] Mic button injected into composer` после успешного inject.
### `package.json`
- `"version": "1.0.0"``"version": "1.0.1"`.
### `vite.config.ts`
- `userscript.version: "1.0.0"``"1.0.1"`.
### Файлы вне спеки
- `src/audio.ts`, `src/transcribe.ts`, `src/config.ts`, `src/keyboard.ts`, `src/types.ts` — без изменений.
## Коммиты
1. `d5f80e7` fix(ui): broaden page detection gate for v1.18.x — `src/insert.ts`
2. `6982bfb` feat(ui): add debug logging for init and composer detection — `src/index.ts`, `src/ui.ts`
3. `af3edc7` chore: bump version to 1.0.1 — `package.json`, `vite.config.ts`
4. `docs(handoff): add handoff + ADR for PR #30` — этот коммит
## ADR
См. `docs/decisions/0002-pr-30-broaden-page-detection-gate.md`.
## Источники
- Предыдущий handoff: `docs/handoff/pr-29-v1-18-composer-selectors.md` (родительский PR, селекторы v1.18.x)
- Предыдущий ADR: `docs/decisions/0001-pr-29-v1-18-composer-selectors-fallback-order.md` (порядок `COMPOSER_SELECTORS`)
- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` (исследование `sst/opencode` main, lazy-mount композера)
- Live-верификация: opencode.slaid098.dev v1.18.3 (DevTools, confirm селекторов v1.18.x)

View file

@ -1,6 +1,6 @@
{ {
"name": "opencode-voice-dictation", "name": "opencode-voice-dictation",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {

View file

@ -174,9 +174,11 @@ function checkFirstRun(): void {
function init(): void { function init(): void {
if (!isOpencodePage()) { if (!isOpencodePage()) {
console.log("[ocvd] OpenCode page not detected, retrying in 1.5s...");
setTimeout(init, 1500); setTimeout(init, 1500);
return; return;
} }
console.log("[ocvd] OpenCode page detected, setting up UI");
ui = setupUI({ ui = setupUI({
onToggle: (target: InsertTarget) => { onToggle: (target: InsertTarget) => {

View file

@ -1,11 +1,19 @@
const PROMPT_INPUT_SELECTOR = '[data-component="prompt-input"]'; const PROMPT_INPUT_SELECTOR = '[data-component="prompt-input"]';
const SUBMIT_SELECTOR = '[data-action="prompt-submit"]'; const SUBMIT_SELECTOR = '[data-action="prompt-submit"]';
const QUESTION_INPUT_SELECTOR = '[data-slot="question-custom-input"]'; const QUESTION_INPUT_SELECTOR = '[data-slot="question-custom-input"]';
const PAGE_DETECT_SELECTORS = [
'[data-component="prompt-input"]',
'[data-component="prompt-input-v2"]',
'[data-component="session-prompt-dock"]',
'[data-component="session-composer"]',
'[data-component="session-new-composer"]',
'[data-slot="question-custom-input"]',
];
export type InsertTarget = "composer" | "question"; export type InsertTarget = "composer" | "question";
export function isOpencodePage(): boolean { export function isOpencodePage(): boolean {
return document.querySelector(PROMPT_INPUT_SELECTOR) !== null; return PAGE_DETECT_SELECTORS.some((s) => document.querySelector(s) !== null);
} }
export function isQuestionPromptOpen(): boolean { export function isQuestionPromptOpen(): boolean {

View file

@ -251,9 +251,11 @@ function findComposer(): HTMLElement | null {
for (const selector of COMPOSER_SELECTORS) { for (const selector of COMPOSER_SELECTORS) {
const el = document.querySelector<HTMLElement>(selector); const el = document.querySelector<HTMLElement>(selector);
if (el) { if (el) {
console.log(`[ocvd] Composer found via ${selector}`);
return el; return el;
} }
} }
console.log("[ocvd] No composer found in DOM");
return null; return null;
} }
@ -301,6 +303,7 @@ function injectIntoComposer(onToggle: (target: InsertTarget) => void, onCancel:
const composer = findComposer(); const composer = findComposer();
if (composer) { if (composer) {
injectIntoElement(composer, onToggle, onCancel, "composer"); injectIntoElement(composer, onToggle, onCancel, "composer");
console.log("[ocvd] Mic button injected into composer");
} }
} }

View file

@ -8,7 +8,7 @@ export default defineConfig({
userscript: { userscript: {
name: "OpenCode Voice Dictation", name: "OpenCode Voice Dictation",
namespace: "https://github.com/slaid098/opencode-voice-dictation", namespace: "https://github.com/slaid098/opencode-voice-dictation",
version: "1.0.0", version: "1.0.1",
description: description:
"Voice dictation for OpenCode web using Whisper (Groq API) - works on PC and mobile", "Voice dictation for OpenCode web using Whisper (Groq API) - works on PC and mobile",
author: "slaid098", author: "slaid098",