diff --git a/docs/decisions/0002-pr-30-broaden-page-detection-gate.md b/docs/decisions/0002-pr-30-broaden-page-detection-gate.md new file mode 100644 index 0000000..63bcf17 --- /dev/null +++ b/docs/decisions/0002-pr-30-broaden-page-detection-gate.md @@ -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) \ No newline at end of file diff --git a/docs/handoff/pr-30-v1-18-runtime-gate-debug.md b/docs/handoff/pr-30-v1-18-runtime-gate-debug.md new file mode 100644 index 0000000..37013dd --- /dev/null +++ b/docs/handoff/pr-30-v1-18-runtime-gate-debug.md @@ -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) \ No newline at end of file diff --git a/package.json b/package.json index d3f99c7..21b576e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-voice-dictation", - "version": "1.0.0", + "version": "1.0.1", "private": true, "type": "module", "engines": { diff --git a/src/index.ts b/src/index.ts index 9e799c7..08d1049 100644 --- a/src/index.ts +++ b/src/index.ts @@ -174,9 +174,11 @@ function checkFirstRun(): void { function init(): void { if (!isOpencodePage()) { + console.log("[ocvd] OpenCode page not detected, retrying in 1.5s..."); setTimeout(init, 1500); return; } + console.log("[ocvd] OpenCode page detected, setting up UI"); ui = setupUI({ onToggle: (target: InsertTarget) => { diff --git a/src/insert.ts b/src/insert.ts index 03b9b6a..3596eef 100644 --- a/src/insert.ts +++ b/src/insert.ts @@ -1,11 +1,19 @@ const PROMPT_INPUT_SELECTOR = '[data-component="prompt-input"]'; const SUBMIT_SELECTOR = '[data-action="prompt-submit"]'; 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 function isOpencodePage(): boolean { - return document.querySelector(PROMPT_INPUT_SELECTOR) !== null; + return PAGE_DETECT_SELECTORS.some((s) => document.querySelector(s) !== null); } export function isQuestionPromptOpen(): boolean { diff --git a/src/ui.ts b/src/ui.ts index 69898e3..190d1a0 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -251,9 +251,11 @@ function findComposer(): HTMLElement | null { for (const selector of COMPOSER_SELECTORS) { const el = document.querySelector(selector); if (el) { + console.log(`[ocvd] Composer found via ${selector}`); return el; } } + console.log("[ocvd] No composer found in DOM"); return null; } @@ -301,6 +303,7 @@ function injectIntoComposer(onToggle: (target: InsertTarget) => void, onCancel: const composer = findComposer(); if (composer) { injectIntoElement(composer, onToggle, onCancel, "composer"); + console.log("[ocvd] Mic button injected into composer"); } } diff --git a/vite.config.ts b/vite.config.ts index 06d9366..572089f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ userscript: { name: "OpenCode Voice Dictation", namespace: "https://github.com/slaid098/opencode-voice-dictation", - version: "1.0.0", + version: "1.0.1", description: "Voice dictation for OpenCode web using Whisper (Groq API) - works on PC and mobile", author: "slaid098",