fix(ui): support OpenCode 1.18.x composer selectors (#29)

## Что сделано
- Обновлён массив 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 для обратной совместимости
- README обновлён с указанием совместимой версии OpenCode v1.18.x
(русский и английский блоки)
- Добавлен handoff-документ и 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).

Closes #28

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-07-25 01:29:00 +03:00 committed by GitHub
parent 8de6ca3c7e
commit af5bc63bcc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 360 additions and 3 deletions

View file

@ -10,7 +10,7 @@
Голосовая диктовка для [OpenCode](https://opencode.ai) web через Whisper (Groq API). Работает на ПК и телефоне. Голосовая диктовка для [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. 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 ### Install

View file

@ -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`

View file

@ -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`

View file

@ -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` (архитектура репо)

View file

@ -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-<N>-<slug>.md` с секциями `## Что сделано`, `## Почему`, `## Pending`, `## Watch out`.
- ADR на архитектурное решение: `docs/handoff/adr/<NNNN>-<slug>.md` с секциями `## Статус`, `## Контекст`, `## Решение`, `## Альтернативы`.
- Селекторы OpenCode UI — primary (новые) → fallback (старые), порядок = приоритет детекции.

21
docs/project-map/docs.md Normal file
View file

@ -0,0 +1,21 @@
---
module: docs
purpose: Handoff-документы, ADR, project map
key_files:
- docs/handoff/pr-<N>-<slug>.md — handoff на PR
- docs/handoff/adr/<NNNN>-<slug>.md — архитектурные решения
- docs/project-map/ — карта структуры проекта
dependencies: []
last_updated: 2026-07-24
---
# docs/
## Structure
- `handoff/pr-<N>-<slug>.md` — handoff на PR (секции: Что сделано, Почему, Pending, Watch out)
- `handoff/adr/<NNNN>-<slug>.md` — архитектурные решения (секции: Статус, Контекст, Решение, Альтернативы)
- `project-map/` — карта структуры проекта (README + по файлу на модуль)
## Patterns
- Один handoff на PR, slug = тема PR.
- ADR нумеруются с 0001.

View file

@ -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` — авто-обновление зависимостей

31
docs/project-map/src.md Normal file
View file

@ -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`.

26
docs/project-map/tests.md Normal file
View file

@ -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 файлов).

View file

@ -6,8 +6,10 @@ const CONTAINER_CLASS = "ocvd-container";
const TIMER_CLASS = "ocvd-timer"; const TIMER_CLASS = "ocvd-timer";
const CANCEL_CLASS = "ocvd-cancel"; const CANCEL_CLASS = "ocvd-cancel";
const COMPOSER_SELECTORS = [ 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-new-composer"]',
'[data-component="session-composer"]',
]; ];
const QUESTION_INPUT_SELECTOR = '[data-slot="question-custom-input"]'; const QUESTION_INPUT_SELECTOR = '[data-slot="question-custom-input"]';