diff --git a/README.md b/README.md index 6526bc4..a9060fa 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Голосовая диктовка для [OpenCode](https://opencode.ai) web через Whisper (Groq API). Работает на ПК и телефоне. -> ⚠️ **Требуется новый интерфейс OpenCode.** В настройках OpenCode web включите «New UI» — в старом интерфейсе скрипт не работает. +> ⚠️ **Требуется OpenCode web v1.18.x.** В настройках OpenCode web включите «New UI» — в старом интерфейсе скрипт не работает. Совместимо с OpenCode v1.18.x; старые версии поддерживаются через fallback-селекторы. ### Установка @@ -65,7 +65,7 @@ Voice dictation for [OpenCode](https://opencode.ai) web via Whisper (Groq API). Works on desktop and mobile. -> ⚠️ **Requires the new OpenCode UI.** In OpenCode web settings, enable "New UI" — the script does not work in the old interface. +> ⚠️ **Requires OpenCode web v1.18.x.** In OpenCode web settings, enable "New UI" — the script does not work in the old interface. Compatible with OpenCode v1.18.x; older versions are supported via fallback selectors. ### Install diff --git a/docs/decisions/0001-pr-29-v1-18-composer-selectors-fallback-order.md b/docs/decisions/0001-pr-29-v1-18-composer-selectors-fallback-order.md new file mode 100644 index 0000000..83a2faa --- /dev/null +++ b/docs/decisions/0001-pr-29-v1-18-composer-selectors-fallback-order.md @@ -0,0 +1,66 @@ +# ADR 0001: COMPOSER_SELECTORS fallback order for OpenCode v1.18.x + +- **Date**: 2026-07-24 +- **PR**: #29 +- **Issue**: #28 + +## Статус + +Accepted. + +## Контекст + +OpenCode web v1.18.x переименовал контейнер композера: +- `session-composer` → `session-new-composer` → **`session-prompt-dock`** (текущий, обёртка региона) +- добавлен **`prompt-input-v2`** (v2 form-контейнер внутри `PromptInputV2Composer`) + +Старые селекторы `session-composer` и `session-new-composer` **полностью удалены** из исходников OpenCode v1.18.x (0 совпадений по grep в `src/`, только упоминания в stories/comments). + +Userscript opencode-voice-dictation использует `findComposer()` в `src/ui.ts` для позиционирования кнопки микрофона. Массив `COMPOSER_SELECTORS` перебирается сверху вниз, возвращается первый найденный элемент. С устаревшими селекторами `findComposer()` возвращал `null` в v1.18.x — кнопка 🎤 не появлялась. + +Дополнительные ограничения: +- `oldInterfaceSunset = 2026-09-14` (`packages/app/src/context/settings.tsx:62`) — legacy UI окончательно уходит, после этой даты fallback на `session-composer` станет бесполезен. +- Home-страница нового layout (`NewHome`) не содержит композера вообще — userscript активен только на `/new-session` или открытых сессиях. +- `promptReady()` lazy-mount: prompt-input появляется асинхронно после загрузки сообщений; MutationObserver в `setupUI` корректно отслеживает. + +## Решение + +Обновить `COMPOSER_SELECTORS` в порядке **новое → старое** (primary → fallback): + +```js +const COMPOSER_SELECTORS = [ + '[data-component="prompt-input-v2"]', // v2 form-контейнер (v1.18.x primary) + '[data-component="session-prompt-dock"]', // v1.18.x регион-обёртка + '[data-component="session-new-composer"]', // pre-1.18 fallback + '[data-component="session-composer"]', // legacy fallback (<1.17) +]; +``` + +## Альтернативы + +### 1. Только новые селекторы (без fallback) +- **Плюс**: проще, нет мёртвого кода после `oldInterfaceSunset`. +- **Минус**: ломает пользователей на OpenCode <1.18 (а это основная установленная база до 14.09.2026). Нарушает acceptance criterion "старые версии остаются совместимы". + +### 2. Только `session-prompt-dock` (без `prompt-input-v2`) +- **Плюс**: один селектор для региона. +- **Минус**: `prompt-input-v2` появляется раньше в lifecycle (form-контейнер монтируется внутри dock'а до того, как dock готов). Пропуск `prompt-input-v2` задерживает появление кнопки. Issue явно требует оба селектора. + +### 3. Обратный порядок (fallback → primary) +- **Минус**: на v1.18.x `findComposer()` сразу вернёт `null` для `session-composer`/`session-new-composer` (они удалены), потом найдёт `session-prompt-dock` — работает, но семантически неверно и менее читаемо. Плюс на гибридных сборках может совпасть устаревший элемент первым. + +## Последствия + +- Кнопка 🎤 появляется в OpenCode v1.18.x (через `prompt-input-v2` или `session-prompt-dock`). +- Обратная совместимость с OpenCode <1.18 сохранена через `session-new-composer` / `session-composer`. +- После `oldInterfaceSunset` (14.09.2026) два последних селектора станут мёртвым кодом — можно удалить отдельным PR, не критично. +- Порядок массива = приоритет детекции; `findComposer()` без изменений. +- Не затрагивает `src/insert.ts` (gate `[data-component="prompt-input"]` остаётся валидным в обеих версиях UI). + +## Источники + +- `sst/opencode` main (commit 2b2aacc, web pkg 1.18.4): + - `packages/app/src/pages/session/composer/session-composer-region.tsx:26` — `session-prompt-dock` + - `packages/session-ui/src/v2/components/prompt-input/index.tsx:107` — `prompt-input-v2` + - `packages/app/src/context/settings.tsx:62` — `oldInterfaceSunset = 2026-09-14` +- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` \ No newline at end of file diff --git a/docs/handoff/adr/0001-pr-29-v1-18-composer-selectors-fallback-order.md b/docs/handoff/adr/0001-pr-29-v1-18-composer-selectors-fallback-order.md new file mode 100644 index 0000000..83a2faa --- /dev/null +++ b/docs/handoff/adr/0001-pr-29-v1-18-composer-selectors-fallback-order.md @@ -0,0 +1,66 @@ +# ADR 0001: COMPOSER_SELECTORS fallback order for OpenCode v1.18.x + +- **Date**: 2026-07-24 +- **PR**: #29 +- **Issue**: #28 + +## Статус + +Accepted. + +## Контекст + +OpenCode web v1.18.x переименовал контейнер композера: +- `session-composer` → `session-new-composer` → **`session-prompt-dock`** (текущий, обёртка региона) +- добавлен **`prompt-input-v2`** (v2 form-контейнер внутри `PromptInputV2Composer`) + +Старые селекторы `session-composer` и `session-new-composer` **полностью удалены** из исходников OpenCode v1.18.x (0 совпадений по grep в `src/`, только упоминания в stories/comments). + +Userscript opencode-voice-dictation использует `findComposer()` в `src/ui.ts` для позиционирования кнопки микрофона. Массив `COMPOSER_SELECTORS` перебирается сверху вниз, возвращается первый найденный элемент. С устаревшими селекторами `findComposer()` возвращал `null` в v1.18.x — кнопка 🎤 не появлялась. + +Дополнительные ограничения: +- `oldInterfaceSunset = 2026-09-14` (`packages/app/src/context/settings.tsx:62`) — legacy UI окончательно уходит, после этой даты fallback на `session-composer` станет бесполезен. +- Home-страница нового layout (`NewHome`) не содержит композера вообще — userscript активен только на `/new-session` или открытых сессиях. +- `promptReady()` lazy-mount: prompt-input появляется асинхронно после загрузки сообщений; MutationObserver в `setupUI` корректно отслеживает. + +## Решение + +Обновить `COMPOSER_SELECTORS` в порядке **новое → старое** (primary → fallback): + +```js +const COMPOSER_SELECTORS = [ + '[data-component="prompt-input-v2"]', // v2 form-контейнер (v1.18.x primary) + '[data-component="session-prompt-dock"]', // v1.18.x регион-обёртка + '[data-component="session-new-composer"]', // pre-1.18 fallback + '[data-component="session-composer"]', // legacy fallback (<1.17) +]; +``` + +## Альтернативы + +### 1. Только новые селекторы (без fallback) +- **Плюс**: проще, нет мёртвого кода после `oldInterfaceSunset`. +- **Минус**: ломает пользователей на OpenCode <1.18 (а это основная установленная база до 14.09.2026). Нарушает acceptance criterion "старые версии остаются совместимы". + +### 2. Только `session-prompt-dock` (без `prompt-input-v2`) +- **Плюс**: один селектор для региона. +- **Минус**: `prompt-input-v2` появляется раньше в lifecycle (form-контейнер монтируется внутри dock'а до того, как dock готов). Пропуск `prompt-input-v2` задерживает появление кнопки. Issue явно требует оба селектора. + +### 3. Обратный порядок (fallback → primary) +- **Минус**: на v1.18.x `findComposer()` сразу вернёт `null` для `session-composer`/`session-new-composer` (они удалены), потом найдёт `session-prompt-dock` — работает, но семантически неверно и менее читаемо. Плюс на гибридных сборках может совпасть устаревший элемент первым. + +## Последствия + +- Кнопка 🎤 появляется в OpenCode v1.18.x (через `prompt-input-v2` или `session-prompt-dock`). +- Обратная совместимость с OpenCode <1.18 сохранена через `session-new-composer` / `session-composer`. +- После `oldInterfaceSunset` (14.09.2026) два последних селектора станут мёртвым кодом — можно удалить отдельным PR, не критично. +- Порядок массива = приоритет детекции; `findComposer()` без изменений. +- Не затрагивает `src/insert.ts` (gate `[data-component="prompt-input"]` остаётся валидным в обеих версиях UI). + +## Источники + +- `sst/opencode` main (commit 2b2aacc, web pkg 1.18.4): + - `packages/app/src/pages/session/composer/session-composer-region.tsx:26` — `session-prompt-dock` + - `packages/session-ui/src/v2/components/prompt-input/index.tsx:107` — `prompt-input-v2` + - `packages/app/src/context/settings.tsx:62` — `oldInterfaceSunset = 2026-09-14` +- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` \ No newline at end of file diff --git a/docs/handoff/pr-29-v1-18-composer-selectors.md b/docs/handoff/pr-29-v1-18-composer-selectors.md new file mode 100644 index 0000000..a85c12a --- /dev/null +++ b/docs/handoff/pr-29-v1-18-composer-selectors.md @@ -0,0 +1,98 @@ +--- +pr_number: 29 +branch: fix/ui/v1-18-composer-selectors +issue: 28 +title: support OpenCode 1.18.x composer selectors +status: open +created: 2026-07-24 +--- + +# Handoff — PR #29: v1.18.x composer selectors + +## Контекст + +Userscript opencode-voice-dictation не вставляет кнопку микрофона в OpenCode web v1.18.x — `findComposer()` в `src/ui.ts` возвращал `null`, потому что массив `COMPOSER_SELECTORS` содержал только устаревшие селекторы `[data-component="session-composer"]` и `[data-component="session-new-composer"]`, которые полностью удалены из исходников OpenCode v1.18.x. + +В OpenCode v1.18.x контейнер композера переименован: +- `[data-component="session-prompt-dock"]` — обёртка региона композера (`packages/app/src/pages/session/composer/session-composer-region.tsx:26`) +- `[data-component="prompt-input-v2"]` — v2 form-контейнер (`packages/session-ui/src/v2/components/prompt-input/index.tsx:107`) + +Уцелели в v1.18.x (НЕ трогались): `[data-component="prompt-input"]` (contenteditable), `[data-action="prompt-submit"]` (кнопка отправки), `[data-slot="question-custom-input"]` (textarea). + +Issue: #28. + +## Что сделано + +- Обновлён массив `COMPOSER_SELECTORS` в `src/ui.ts`: добавлены новые селекторы `[data-component="prompt-input-v2"]` и `[data-component="session-prompt-dock"]` как primary для OpenCode v1.18.x (первые в массиве). +- Старые селекторы `session-new-composer` / `session-composer` сохранены как fallback для обратной совместимости с OpenCode <1.18. +- `README.md` обновлён с указанием совместимой версии OpenCode v1.18.x (русский и английский блоки). +- Добавлен ADR 0001 с обоснованием порядка fallback-селекторов. + +## Почему + +В OpenCode v1.18.x селекторы `session-composer` / `session-new-composer` полностью удалены из исходников, из-за чего `findComposer()` возвращал `null` и кнопка 🎤 не появлялась в чате. Порядок массива обновлён как primary (v1.18.x) → fallback (pre-1.18), чтобы сохранить совместимость с OpenCode <1.18 до `oldInterfaceSunset` (2026-09-14). Функция `findComposer()` уже перебирает массив сверху вниз — изменений в логике не потребовалось. + +## Pending + +- После `oldInterfaceSunset` (2026-09-14) legacy-селекторы `session-new-composer` / `session-composer` станут мёртвым кодом — удалить отдельным PR. +- Отслеживать дальнейшие переименования контейнера композера в OpenCode (источник: `sst/opencode` main). + +## Watch out + +- Порядок `COMPOSER_SELECTORS` = приоритет детекции; не менять без обновления ADR 0001. +- `prompt-input-v2` появляется раньше `session-prompt-dock` в lifecycle (form-контейнер монтируется внутри dock'а) — порядок primary→fallback важен для раннего появления кнопки. +- Home-страница нового layout (`NewHome`) не содержит композера — userscript активен только на `/new-session` или открытых сессиях. +- `src/insert.ts` не затронут: gate `[data-component="prompt-input"]` остаётся валидным в обеих версиях UI. + +## Изменения + +### `src/ui.ts` +- Обновлён массив `COMPOSER_SELECTORS` (новые селекторы v1.18.x первыми как primary, старые сохранены как fallback): + ```js + const COMPOSER_SELECTORS = [ + '[data-component="prompt-input-v2"]', + '[data-component="session-prompt-dock"]', + '[data-component="session-new-composer"]', + '[data-component="session-composer"]', + ]; + ``` +- Функция `findComposer()` уже перебирает массив — изменений не потребовалось. + +### `README.md` +- Строка 13 (русский блок): предупреждение о "New UI" уточнено до совместимой версии OpenCode v1.18.x. +- Строка 68 (english-блок): аналогичное уточнение для английского блока. + +### Файлы вне спеки +- `src/insert.ts`, `src/audio.ts`, `src/transcribe.ts`, `src/config.ts`, `src/keyboard.ts`, `src/types.ts`, `src/index.ts` — без изменений. + +## Проверки + +| Проверка | Команда | Результат | +|----------|---------|----------| +| Lint | `npm run lint` (biome check) | ✅ Checked 21 files, no fixes | +| Typecheck | `npm run typecheck` (tsc --noEmit) | ✅ No errors | +| Tests | `npm run test` (vitest run) | ✅ 47/47 passed (5 files) | +| Knip | `npm run knip` | ✅ No unused exports | + +## Acceptance criteria + +- [x] Кнопка 🎤 появляется в OpenCode web v1.18.x (через новые primary-селекторы `prompt-input-v2` / `session-prompt-dock`) +- [x] Старые версии OpenCode остаются совместимы (fallback-селекторы `session-new-composer` / `session-composer` сохранены) +- [x] `npm run lint && npm run typecheck && npm run test && npm run knip` проходят +- [x] README обновлён с указанием совместимой версии OpenCode v1.18.x (русский и английский блоки) + +## Коммиты + +1. `fix(ui): add v1.18.x composer selectors` — обновление `COMPOSER_SELECTORS` в `src/ui.ts` +2. `docs(readme): specify OpenCode v1.18.x compatibility` — уточнение совместимости в обоих языковых блоках README +3. `docs(handoff): add handoff + ADR for PR` — handoff-документ и ADR +4. `docs(handoff): set PR number` — подстановка реального номера PR (после `gh pr create`) + +## ADR + +См. `docs/handoff/adr/0001-pr-29-v1-18-composer-selectors-fallback-order.md`. + +## Источники + +- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` (исследование `sst/opencode` main commit 2b2aacc, web pkg 1.18.4) +- Память: `repos/opencode-voice-dictation.md` (архитектура репо) \ No newline at end of file diff --git a/docs/project-map/README.md b/docs/project-map/README.md new file mode 100644 index 0000000..4e00ef4 --- /dev/null +++ b/docs/project-map/README.md @@ -0,0 +1,30 @@ +--- +module: . +purpose: Index of project structure and top-level modules +last_updated: 2026-07-24 +--- + +# Project Map — opencode-voice-dictation + +Userscript для голосовой диктовки в OpenCode web через Whisper (Groq API). Точки входа в UI OpenCode определяются через массив `COMPOSER_SELECTORS` в `src/ui.ts` (primary для v1.18.x + fallback для pre-1.18). + +## Top-level modules + +- [`src/`](./src.md) — исходники userscript (entry, ui, audio, transcribe, insert, config, keyboard, types) +- [`tests/`](./tests.md) — unit-тесты (vitest) + моки +- [`.github/`](./github.md) — CI workflows, dependabot, release +- [`docs/`](./docs.md) — handoff, ADR, project map + +## Build / tooling + +- `vite.config.ts` + `vite-plugin-monkey` — сборка userscript (.user.js) +- `biome.json` — lint/format +- `tsconfig.json` — TypeScript +- `knip.json` — детектор неиспользуемого кода +- `vitest.config.ts` — test runner + +## Conventions + +- Один handoff на PR: `docs/handoff/pr--.md` с секциями `## Что сделано`, `## Почему`, `## Pending`, `## Watch out`. +- ADR на архитектурное решение: `docs/handoff/adr/-.md` с секциями `## Статус`, `## Контекст`, `## Решение`, `## Альтернативы`. +- Селекторы OpenCode UI — primary (новые) → fallback (старые), порядок = приоритет детекции. \ No newline at end of file diff --git a/docs/project-map/docs.md b/docs/project-map/docs.md new file mode 100644 index 0000000..bb09c64 --- /dev/null +++ b/docs/project-map/docs.md @@ -0,0 +1,21 @@ +--- +module: docs +purpose: Handoff-документы, ADR, project map +key_files: + - docs/handoff/pr--.md — handoff на PR + - docs/handoff/adr/-.md — архитектурные решения + - docs/project-map/ — карта структуры проекта +dependencies: [] +last_updated: 2026-07-24 +--- + +# docs/ + +## Structure +- `handoff/pr--.md` — handoff на PR (секции: Что сделано, Почему, Pending, Watch out) +- `handoff/adr/-.md` — архитектурные решения (секции: Статус, Контекст, Решение, Альтернативы) +- `project-map/` — карта структуры проекта (README + по файлу на модуль) + +## Patterns +- Один handoff на PR, slug = тема PR. +- ADR нумеруются с 0001. \ No newline at end of file diff --git a/docs/project-map/github.md b/docs/project-map/github.md new file mode 100644 index 0000000..3229cfe --- /dev/null +++ b/docs/project-map/github.md @@ -0,0 +1,17 @@ +--- +module: .github +purpose: CI workflows, dependabot, release +key_files: + - .github/workflows/ci.yml — CI (lint, typecheck, test, knip) + - .github/workflows/release.yml — релиз + GitHub Release + - .github/dependabot.yml — авто-обновление зависимостей +dependencies: [] +last_updated: 2026-07-24 +--- + +# .github/ + +## Structure +- `workflows/ci.yml` — CI (lint, typecheck, test, knip) +- `workflows/release.yml` — релиз + GitHub Release +- `dependabot.yml` — авто-обновление зависимостей \ No newline at end of file diff --git a/docs/project-map/src.md b/docs/project-map/src.md new file mode 100644 index 0000000..43699ba --- /dev/null +++ b/docs/project-map/src.md @@ -0,0 +1,31 @@ +--- +module: src +purpose: Исходники userscript — UI, аудио-захват, транскрипция, вставка текста +key_files: + - src/index.ts — entry point userscript + - src/ui.ts — позиционирование кнопки 🎤, COMPOSER_SELECTORS, findComposer() + - src/audio.ts — захват микрофона (MediaRecorder) + - src/transcribe.ts — отправка в Groq Whisper API + - src/insert.ts — вставка транскрипта в prompt-input + - src/config.ts — настройки (API key, hotkey, language) + - src/keyboard.ts — глобальный hotkey + - src/types.ts — shared типы +dependencies: [] +last_updated: 2026-07-24 +--- + +# src/ + +## Structure +- `index.ts` — entry point userscript +- `ui.ts` — позиционирование кнопки 🎤, `COMPOSER_SELECTORS`, `findComposer()`, MutationObserver +- `audio.ts` — захват микрофона (MediaRecorder) +- `transcribe.ts` — отправка в Groq Whisper API +- `insert.ts` — вставка транскрипта в prompt-input (gate `[data-component="prompt-input"]`) +- `config.ts` — настройки (API key, hotkey, language) +- `keyboard.ts` — глобальный hotkey +- `types.ts` — shared типы + +## Patterns +- Селекторы OpenCode UI собраны в массив `COMPOSER_SELECTORS` (primary → fallback), `findComposer()` перебирает сверху вниз. +- UI-элементы появляются асинхронно — отслеживаются через MutationObserver в `setupUI`. \ No newline at end of file diff --git a/docs/project-map/tests.md b/docs/project-map/tests.md new file mode 100644 index 0000000..6c4611e --- /dev/null +++ b/docs/project-map/tests.md @@ -0,0 +1,26 @@ +--- +module: tests +purpose: Unit-тесты (vitest) + моки +key_files: + - tests/__mocks__/$/index.ts — моки + - tests/audio.test.ts — тесты audio + - tests/config.test.ts — тесты config + - tests/insert.test.ts — тесты insert + - tests/keyboard.test.ts — тесты keyboard + - tests/transcribe.test.ts — тесты transcribe +dependencies: [src] +last_updated: 2026-07-24 +--- + +# tests/ + +## Structure +- `__mocks__/$/index.ts` — моки +- `audio.test.ts` — тесты audio +- `config.test.ts` — тесты config +- `insert.test.ts` — тесты insert +- `keyboard.test.ts` — тесты keyboard +- `transcribe.test.ts` — тесты transcribe + +## Patterns +- vitest run, 47 тестов (5 файлов). \ No newline at end of file diff --git a/src/ui.ts b/src/ui.ts index 2652c73..69898e3 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -6,8 +6,10 @@ const CONTAINER_CLASS = "ocvd-container"; const TIMER_CLASS = "ocvd-timer"; const CANCEL_CLASS = "ocvd-cancel"; const COMPOSER_SELECTORS = [ - '[data-component="session-composer"]', + '[data-component="prompt-input-v2"]', + '[data-component="session-prompt-dock"]', '[data-component="session-new-composer"]', + '[data-component="session-composer"]', ]; const QUESTION_INPUT_SELECTOR = '[data-slot="question-custom-input"]';