diff --git a/docs/decisions/0004-pr-33-child-session-composer-guard.md b/docs/decisions/0004-pr-33-child-session-composer-guard.md new file mode 100644 index 0000000..71e4483 --- /dev/null +++ b/docs/decisions/0004-pr-33-child-session-composer-guard.md @@ -0,0 +1,101 @@ +# ADR 0004: Composer-target guard checks for real input inside dock (child session) + +- **Date**: 2026-07-26 +- **PR**: 33 +- **Parent PR**: #31 +- **Issue**: #32 + +## Статус + +Accepted. + +## Контекст + +OpenCode web (`anomalyco/opencode` dev, `packages/app/src/pages/session/composer/session-composer-region.tsx`) использует `
` как общий wrapper-контейнер для трёх условно-рендеримых дочерних блоков: + +1. `` → `` — уточняющий вопрос от агента (PR #31 закрыл этот case). +2. `` → disabled-блок "Prompt is disabled / Back to parent" — child session (subagent, `session.parentID` заполнен). +3. `` → `PromptInput` / `PromptInputV2Composer` — основной композер ввода. + +`controller.showComposer()` = `!state.blocked() || !!parentID()`. В child session `parentID()` truthy → `showComposer()` всегда truthy → `session-prompt-dock` рендерится. Но `controller.child()` тоже truthy → disabled-блок (ветка 2), composer (ветка 3) НЕ монтируется. `prompt-input` / `prompt-input-v2` отсутствуют внутри dock'а. + +`findComposer()` в `src/ui.ts` перебирает `COMPOSER_SELECTORS` (`prompt-input-v2` → `session-prompt-dock` → `session-new-composer` → `session-composer`, ADR 0001). В child session `prompt-input-v2` скрыт (первый промах), `session-prompt-dock` присутствует (второй успех). Guard PR #31 (skip dock если внутри `session-question-dock`) не срабатывал — внутри dock'а disabled-блок, не question-dock. → `findComposer` возвращал dock как composer-target. + +`injectIntoElement` делал dock `position: relative` и append'ил контейнер с кнопкой 🎤 (`position: absolute; top: 8px; right: 8px`) в правый-верхний угол wrapper'а — над disabled-блоком. При клике → `toggleDictation("composer")` → `insertIntoContenteditable()` → `document.querySelector('[data-component="prompt-input"]')` = `null` → toast "Could not find input field". Кнопка бесполезна, но видна. + +Дополнительные ограничения: +- `session-prompt-dock` нельзя удалить из `COMPOSER_SELECTORS` — когда question-dock НЕ открыт и session НЕ child, это валидный fallback-target (пока `prompt-input-v2` lazy-mount'ится, ADR 0001). +- `injectIntoQuestionPrompts` должен продолжать работать независимо от `injectIntoComposer`. +- `match: ["*://*/*"]` + `run-at: document-idle` — userscript работает на всех сайтах, gate (`PAGE_DETECT_SELECTORS`, ADR 0002) отсеивает не-OpenCode. + +## Решение + +**Defence-in-depth: guard на наличие реального composer внутри dock + guard на уровне документа + бамп версии.** + +**1. Guard в `findComposer()` (`src/ui.ts`).** Skip `session-prompt-dock` в цикле `COMPOSER_SELECTORS`, если внутри НЕТ `[data-component="prompt-input"]` или `[data-component="prompt-input-v2"]`: + +```ts +if ( + el.getAttribute("data-component") === "session-prompt-dock" && + !el.querySelector('[data-component="prompt-input"], [data-component="prompt-input-v2"]') +) { + continue; +} +``` + +В child session внутри dock'а только disabled-блок — guard срабатывает, цикл `continue` переходит к legacy-селекторам (удалены в v1.18.x), `findComposer()` возвращает `null`. Wrapper остаётся в массиве (нужен в нормальном состоянии), но не возвращается как composer-target когда внутри нет реального composer. + +**2. Guard в `injectIntoComposer()` (`src/ui.ts`).** Ранний return, если в документе НЕТ `[data-component="prompt-input"]` или `[data-component="prompt-input-v2"]`: + +```ts +if ( + !document.querySelector('[data-component="prompt-input"], [data-component="prompt-input-v2"]') +) { + return; +} +``` + +Defence-in-depth: защищает от любых future-случаев отсутствия реального composer (не только child session). Срабатывает на уровне документа, независим от `findComposer`. + +**3. Бамп `@version` 1.0.2 → 1.0.3** в `package.json` и `vite.config.ts` (синхронно, ADR 0002 gotcha). Разблокировать автообновление userscript-менеджеров. + +Два guard'а независимы: любой один достаточно для типичного сценария, второй — страховка на race conditions и partial DOM state. Удаление любого сужает coverage, но не ломает основной фикс. + +## Альтернативы + +### 1. Детектировать child session по disabled-блоку (текст "Prompt is disabled" / кнопка "Back to parent") +- **Плюс**: явная детекция именно child session. +- **Минус**: хрупко — зависит от локали (i18n ключи `session.child.promptDisabled` / `session.child.backToParent`), текст может меняться между версиями. Проверка отсутствия `prompt-input` внутри dock — структурная, не зависит от текста. Предпочтительнее. + +### 2. Убрать `session-prompt-dock` из `COMPOSER_SELECTORS` +- **Плюс**: минимальная правка — одна строка. +- **Минус**: `prompt-input-v2` lazy-mount'ится асинхронно (ADR 0001). Между появлением `session-prompt-dock` и `prompt-input-v2` есть окно (до сотен ms), когда `findComposer()` вернёт `null` → кнопка 🎤 не появится до готовности `prompt-input-v2`. Регрессия раннего появления кнопки (PR #29 explicitly добавил `session-prompt-dock` как fallback). Текущее решение сохраняет fallback, но отключает только когда он вреден. + +### 3. CSS-only через `:has()` — скрыть кнопку когда внутри dock нет composer +- **Плюс**: ноль правок в JS. +- **Минус**: маскирует симптом, не лечит root cause. Контейнер остаётся в DOM, `injectIntoElement` guard `if (existing) return null` предотвратит правильную вставку после возврата в parent session (контейнер «существует», просто скрыт). UX regression. CSS-only не подходит для lifecycle-управления (аналогично ADR 0003 альтернативе 2). + +### 4. MutationObserver cleanup застрявших кнопок +- **Плюс**: лечит и текущий баг, и будущие edge-cases. +- **Минус**: сложнее, не лечит root cause — кнопка всё равно вставляется в неверный target, просто быстрее убирается. Defence-in-depth через guards предотвращает вставку, что проще и надёжнее. Cleanup — кандидат на отдельный PR если обнаружатся другие сценарии (аналогично ADR 0003). + +## Последствия + +- `findComposer()` больше не возвращает `session-prompt-dock` как composer-target когда внутри нет `prompt-input`/`prompt-input-v2` — цикл `continue` пропускает его и переходит к legacy-селекторам (удалены в v1.18.x). В child session `findComposer` вернёт `null` — `injectIntoComposer` ничего не вставит. +- `injectIntoComposer()` имеет ранний return при отсутствии реального composer в документе — даже если `findComposer` найдёт валидный target (edge-case), вставка откладывается до появления `prompt-input`/`prompt-input-v2`. На следующем MutationObserver-цикле вставка проходит нормально. +- `@version` 1.0.3 в `.meta.js` разблокирует автообновление userscript-менеджеров. +- Два guard'а независимы: удаление любого оставляет race-condition window, но не ломает основной фикс. Не объединять в одну проверку — они защищают разные слои (target selection vs insertion). +- `session-prompt-dock` остаётся в `COMPOSER_SELECTORS` как fallback для нормального состояния — не удалять без замены. +- Застрявшие кнопки у текущих пользователей (на 1.0.2) не удаляются автоматически — после автообновления до 1.0.3 новые вставки не происходят, но старые остаются до hard-refresh. Cosmetic, не blocking (аналогично PR #31). +- Guard устойчив к future-вариантам: любой сценарий где `session-prompt-dock` есть, а `prompt-input`/`prompt-input-v2` внутри нет — пропустит dock. Не привязан к конкретной структуре disabled-блока. + +## Источники + +- `src/ui.ts` (текущий): `findComposer()`, `injectIntoComposer()`, `COMPOSER_SELECTORS`, `injectIntoElement` +- `anomalyco/opencode` dev, `packages/app/src/pages/session/composer/session-composer-region.tsx` (`session-prompt-dock` wrapper, `` disabled-блок) +- `anomalyco/opencode` dev, `packages/app/src/pages/session/composer/session-composer-region-controller.ts` (`child()` = `!!parentID()`, `showComposer()` = `!blocked() || !!parentID()`) +- Предыдущий ADR: `docs/decisions/0001-pr-29-v1-18-composer-selectors-fallback-order.md` (`COMPOSER_SELECTORS` порядок, `session-prompt-dock` как fallback) +- Предыдущий ADR: `docs/decisions/0003-pr-31-question-dock-duplicate-button.md` (defence-in-depth паттерн, `session-question-dock` guard) +- Память: `technical/opencode-web-ui-child-session-composer-disabled.md` (child session рендерит disabled-блок, варианты фикса) +- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` (`session-prompt-dock` = общий wrapper, lazy-mount) +- Issue: #32 \ No newline at end of file diff --git a/docs/handoff/pr-33-child-session-disabled-composer.md b/docs/handoff/pr-33-child-session-disabled-composer.md new file mode 100644 index 0000000..b94d179 --- /dev/null +++ b/docs/handoff/pr-33-child-session-disabled-composer.md @@ -0,0 +1,108 @@ +--- +pr_number: 33 +branch: fix/ui/child-session-composer-guard +parent_pr: 31 +title: skip composer injection in child session (subagent) +status: open +created: 2026-07-26 +--- + +# Handoff — PR 33: skip composer injection in child session + +## Контекст + +Userscript `opencode-voice-dictation` добавляет кнопку 🎤 в composer OpenCode web UI. В **child session** (subagent, созданный через `task` tool) composer заменяется на disabled-блок "Prompt is disabled / Back to parent", но `session-prompt-dock` (внешний wrapper) остаётся в DOM (`showComposer()` = `!blocked() || !!parentID()` → всегда truthy в child). `findComposer()` в `src/ui.ts` использовал этот wrapper как валидный composer-target — кнопка 🎤 рендерилась над disabled-блоком, а клик падал с toast "Could not find input field" (нет `prompt-input` внутри dock'а, `insertIntoContenteditable()` возвращает `false`). + +Это тот же класс бага, что и PR #31 (question-dock duplicate button) — `session-prompt-dock` используется как composer-target в ситуации, когда реальный composer внутри не отрендерен. PR #31 пофиксил только question-dock case (guard: skip dock если внутри `session-question-dock`), child-session case остался latent. Корневая причина та же: wrapper валиден как fallback-target в _нормальном_ состоянии, но вреден когда внутри нет реального composer. + +Issue: #32. Предшественник: PR #31 (`docs/handoff/pr-31-question-dock-duplicate-button.md`). + +## Что сделано + +Три кодовых коммита на ветке `fix/ui/child-session-composer-guard`: + +- **`fix(ui): skip composer injection in child session`** — `src/ui.ts` `findComposer()`: добавлен guard — `session-prompt-dock` skip'ается в цикле `COMPOSER_SELECTORS`, если внутри него НЕТ `[data-component="prompt-input"]` или `[data-component="prompt-input-v2"]`. В child session внутри dock'а только disabled-блок "Prompt is disabled / Back to parent" — guard срабатывает, цикл `continue` переходит к legacy-селекторам (`session-new-composer` / `session-composer`, удалены в v1.18.x), `findComposer()` возвращает `null`. Дополнительно `injectIntoComposer()`: ранний return если в документе нет `[data-component="prompt-input"], [data-component="prompt-input-v2"]` — defence-in-depth, страховка от future-случаев отсутствия реального composer (не только child session). +- **`chore: bump version to 1.0.3`** — `package.json` и `vite.config.ts` синхронно 1.0.2 → 1.0.3. Разблокировать автообновление userscript-менеджеров (ловушка PR #29 → #30: рассинхрон `@version` ломает `updateURL`). +- **`test(ui): cover dock without prompt-input case`** — новый `tests/ui.test.ts` (5 кейсов): dock с `prompt-input-v2` → кнопка есть; dock с `prompt-input` → кнопка есть; dock без composer (child session disabled-блок) → кнопки нет; пустой dock → кнопки нет; question-dock открыт → composer-кнопки нет (PR #31 regression guard). Тестирует через публичный `setupUI` + side-effect (`.ocvd-btn` в DOM), `findComposer` не экспортируется. + +Четвёртый коммит — docs (этот handoff + ADR 0004). + +## Почему + +- **`session-prompt-dock` — общий wrapper для трёх состояний.** В `anomalyco/opencode` dev (`packages/app/src/pages/session/composer/session-composer-region.tsx`) `session-prompt-dock` div рендерится всегда когда `showComposer()` truthy. Внутри условно: `` → `SessionQuestionDock`, `` → disabled-блок "Prompt is disabled / Back to parent", `` → `PromptInput`/`PromptInputV2Composer`. В child session (`session.parentID` заполнен) `child()=true` → disabled-блок, composer НЕ монтируется, но `session-prompt-dock` остаётся. +- **`findComposer()` выбирал wrapper.** `COMPOSER_SELECTORS` = [`prompt-input-v2`, `session-prompt-dock`, `session-new-composer`, `session-composer`] (ADR 0001). В child session `prompt-input-v2` отсутствует (первый промах), `session-prompt-dock` присутствует (второй успех). Guard PR #31 (skip если внутри `session-question-dock`) не срабатывал — внутри dock'а disabled-блок, не question-dock. → `findComposer` возвращал dock как composer-target. +- **Клик падал с toast.** `injectIntoElement` делал dock `position: relative` и append'ил контейнер с кнопкой (`position: absolute; top: 8px; right: 8px`) в правый-верхний угол wrapper'а — над disabled-блоком. При клике → `toggleDictation("composer")` → `insertIntoContenteditable()` → `document.querySelector('[data-component="prompt-input"]')` = `null` → toast "Could not find input field". Кнопка бесполезна, но видна. +- **Defence-in-depth.** Guard в `findComposer` (не возвращать dock без composer внутри) + guard в `injectIntoComposer` (не вставлять если в документе нет `prompt-input`/`prompt-input-v2`). Оба слоя независимы — любой один достаточно для фикса, второй страховка на edge-cases (race conditions, partial DOM state, future-варианты отсутствия composer). Аналог паттерна PR #31. + +## Pending + +- Live-verify на opencode.slaid098.dev после merge: открыть main session → вызвать subagent через `task` → дождаться перехода в child session → проверить, что над disabled-блоком "Prompt is disabled / Back to parent" НЕТ кнопки 🎤. Вернуться в parent session → проверить, что в композере кнопка 🎤 появилась (регресса нет). +- Подтвердить, что userscript-менеджеры подтянули автообновление: сравнить `@version` в `about:` для установленного скрипта с `1.0.3`. Если менеджер не видит обновление — проверить, что `updateURL` в `vite.config.ts` указывает на свежий `.meta.js` в ветке `dist/` (или `main` после merge). +- Проверить regression PR #31: открыть сессию → дождаться вопроса от агента → кликнуть «Свой ответ» → проверить, что composer-кнопки сверху блока вопросов НЕТ (только question-кнопка в textarea). +- После `oldInterfaceSunset` (2026-09-14) legacy-селекторы `session-composer` / `session-new-composer` в `COMPOSER_SELECTORS` и `PAGE_DETECT_SELECTORS` станут мёртвым кодом — удалить отдельным PR (общая pending-задача с PR #29/#30/#31). + +## Watch out + +- **`session-prompt-dock` остаётся в `COMPOSER_SELECTORS` как fallback.** НЕ удалять — когда question-dock НЕ открыт и session НЕ child, `session-prompt-dock` — валидный composer-target (пока `prompt-input-v2` lazy-mount'ится, ADR 0001). Удаление селектора сломало бы раннее появление кнопки на свежих сессиях. Guard пропускает dock только когда внутри нет реального composer. +- **Два guard'а независимы — не объединять.** Guard в `findComposer` (skip dock без composer) и guard в `injectIntoComposer` (skip insert без composer в документе) защищают разные слои: первый предотвращает выбор неверного target, второй — неверную вставку даже если target выбран (edge-case: `prompt-input-v2` в transition state). Удаление любого оставляет race-condition window. +- **Guard в `injectIntoComposer` может задержать появление composer-кнопки на 1 кадр при переходе в parent session.** Если child session закрывается (disabled-блок ушёл, но `prompt-input-v2` ещё не смонтировался), `injectIntoComposer` вернёт `null` от `findComposer()`, следующий retry-цикл MutationObserver поймает появление composer. На практике незаметно (~16ms). +- **Нет cleanup застрявших кнопок у текущих пользователей.** Если баг уже проявился (кнопка вставлена в dock в child session), после автообновления до 1.0.3 новые вставки не происходят, но старые остаются до hard-refresh. Cosmetic, не blocking. Аналогично PR #31. +- **`findComposer` не экспортируется.** Тест `tests/ui.test.ts` тестирует через публичный `setupUI` + side-effect (`.ocvd-btn` в DOM), не через прямой вызов `findComposer`. Это интеграционный тест, но покрывает end-to-end сценарий бага. + +## Изменения + +### `src/ui.ts` + +`findComposer()` — добавлен skip-блок для `session-prompt-dock` без реального composer внутри: +```ts +// session-prompt-dock валиден как composer-target ТОЛЬКО если внутри есть реальный composer; +// child session (subagent) рендерит disabled-блок "Prompt is disabled / Back to parent" вместо composer +if ( + el.getAttribute("data-component") === "session-prompt-dock" && + !el.querySelector('[data-component="prompt-input"], [data-component="prompt-input-v2"]') +) { + continue; +} +``` + +`injectIntoComposer()` — добавлен ранний return при отсутствии реального composer в документе: +```ts +// Defence-in-depth: не вставлять, если в документе нет реального composer (child session / disabled-блок) +if ( + !document.querySelector('[data-component="prompt-input"], [data-component="prompt-input-v2"]') +) { + return; +} +``` + +### `package.json` +- `"version": "1.0.2"` → `"version": "1.0.3"`. + +### `vite.config.ts` +- `userscript.version: "1.0.2"` → `"1.0.3"`. + +### `tests/ui.test.ts` (новый) +- 5 кейсов: dock с composer → кнопка есть; dock без composer (child session) → кнопки нет; пустой dock → кнопки нет; question-dock → composer-кнопки нет. + +### Файлы вне спеки +- `src/audio.ts`, `src/transcribe.ts`, `src/config.ts`, `src/keyboard.ts`, `src/types.ts`, `src/insert.ts`, `src/index.ts` — без изменений. + +## Коммиты + +1. `fix(ui): skip composer injection in child session` — `src/ui.ts` (findComposer + injectIntoComposer) +2. `chore: bump version to 1.0.3` — `package.json` + `vite.config.ts` +3. `test(ui): cover dock without prompt-input case` — `tests/ui.test.ts` +4. `docs(handoff): add child-session composer guard handoff` — этот handoff + ADR 0004 +5. `docs(handoff): set PR number` — после получения PR номера + +## ADR + +См. `docs/decisions/0004-pr-33-child-session-composer-guard.md`. + +## Источники + +- Предыдущий handoff: `docs/handoff/pr-31-question-dock-duplicate-button.md` (родительский PR, тот же класс бага) +- Предыдущий ADR: `docs/decisions/0003-pr-31-question-dock-duplicate-button.md` (defence-in-depth паттерн) +- Память: `technical/opencode-web-ui-child-session-composer-disabled.md` (child session рендерит disabled-блок, `session-composer-region.tsx` ``) +- Память: `technical/opencode-web-ui-composer-selectors-1.18.4.md` (`session-prompt-dock` как wrapper, lazy-mount) +- Issue: #32 \ No newline at end of file diff --git a/docs/project-map/tests.md b/docs/project-map/tests.md index 6c4611e..46f48ce 100644 --- a/docs/project-map/tests.md +++ b/docs/project-map/tests.md @@ -8,8 +8,9 @@ key_files: - tests/insert.test.ts — тесты insert - tests/keyboard.test.ts — тесты keyboard - tests/transcribe.test.ts — тесты transcribe + - tests/ui.test.ts — тесты UI-инъекции кнопки 🎤 (composer-dock, child session, question-dock) dependencies: [src] -last_updated: 2026-07-24 +last_updated: 2026-07-26 --- # tests/ @@ -21,6 +22,8 @@ last_updated: 2026-07-24 - `insert.test.ts` — тесты insert - `keyboard.test.ts` — тесты keyboard - `transcribe.test.ts` — тесты transcribe +- `ui.test.ts` — тесты UI-инъекции кнопки 🎤 (5 кейсов: dock с prompt-input-v2/prompt-input → кнопка есть; dock без composer / пустой dock / question-dock → кнопки нет). Тестирует через публичный `setupUI` + side-effect (`.ocvd-btn` в DOM), `findComposer` не экспортируется. ## Patterns -- vitest run, 47 тестов (5 файлов). \ No newline at end of file +- vitest run, 52 теста (6 файлов). +- UI-тесты используют интеграционный подход через `setupUI` + проверку side-effect в DOM (`.ocvd-btn`), т.к. `findComposer` не экспортируется. \ No newline at end of file diff --git a/package.json b/package.json index 1ae0e73..3706af3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-voice-dictation", - "version": "1.0.2", + "version": "1.0.3", "private": true, "type": "module", "engines": { diff --git a/src/ui.ts b/src/ui.ts index c13dd57..0ef16c0 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -251,7 +251,7 @@ function findComposer(): HTMLElement | null { for (const selector of COMPOSER_SELECTORS) { const el = document.querySelector(selector); if (el) { - // session-prompt-dock — общий wrapper для question-dock и composer; + // session-prompt-dock — общий wrapper для question-dock, composer и child-session disabled-блока; // если внутри question-dock, не использовать как composer-target if ( el.getAttribute("data-component") === "session-prompt-dock" && @@ -259,6 +259,14 @@ function findComposer(): HTMLElement | null { ) { continue; } + // session-prompt-dock валиден как composer-target ТОЛЬКО если внутри есть реальный composer; + // child session (subagent) рендерит disabled-блок "Prompt is disabled / Back to parent" вместо composer + if ( + el.getAttribute("data-component") === "session-prompt-dock" && + !el.querySelector('[data-component="prompt-input"], [data-component="prompt-input-v2"]') + ) { + continue; + } console.log(`[ocvd] Composer found via ${selector}`); return el; } @@ -312,6 +320,12 @@ function injectIntoComposer(onToggle: (target: InsertTarget) => void, onCancel: if (document.querySelector('[data-slot="question-custom-input"]')) { return; } + // Defence-in-depth: не вставлять, если в документе нет реального composer (child session / disabled-блок) + if ( + !document.querySelector('[data-component="prompt-input"], [data-component="prompt-input-v2"]') + ) { + return; + } const composer = findComposer(); if (composer) { injectIntoElement(composer, onToggle, onCancel, "composer"); diff --git a/tests/ui.test.ts b/tests/ui.test.ts new file mode 100644 index 0000000..3cf27dd --- /dev/null +++ b/tests/ui.test.ts @@ -0,0 +1,71 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { setupUI } from "../src/ui.js"; + +const COMPOSER_BTN_SELECTOR = ".ocvd-btn"; + +function setupCallbacks() { + return { + onToggle: vi.fn(), + onCancel: vi.fn(), + }; +} + +afterEach(() => { + document.body.innerHTML = ""; +}); + +describe("setupUI composer injection", () => { + beforeEach(() => { + vi.spyOn(console, "log").mockImplementation(() => {}); + }); + + it("should inject mic button when prompt-input-v2 is present", () => { + document.body.innerHTML = + '
'; + + setupUI(setupCallbacks()); + + const btn = document.querySelector(COMPOSER_BTN_SELECTOR); + expect(btn).not.toBeNull(); + }); + + it("should inject mic button when prompt-input is present inside dock", () => { + document.body.innerHTML = + '
'; + + setupUI(setupCallbacks()); + + const btn = document.querySelector(COMPOSER_BTN_SELECTOR); + expect(btn).not.toBeNull(); + }); + + it("should NOT inject mic button when dock has no prompt-input (child session disabled block)", () => { + document.body.innerHTML = + '
Prompt is disabled
'; + + setupUI(setupCallbacks()); + + const btn = document.querySelector(COMPOSER_BTN_SELECTOR); + expect(btn).toBeNull(); + }); + + it("should NOT inject mic button when only session-prompt-dock exists without any composer inside", () => { + document.body.innerHTML = '
'; + + setupUI(setupCallbacks()); + + const btn = document.querySelector(COMPOSER_BTN_SELECTOR); + expect(btn).toBeNull(); + }); + + it("should NOT inject composer mic button when question-custom-input is open (PR #31 regression guard)", () => { + document.body.innerHTML = + '
'; + + setupUI(setupCallbacks()); + + const dock = document.querySelector('[data-component="session-prompt-dock"]'); + const composerContainer = dock?.querySelector(":scope > .ocvd-container"); + expect(composerContainer).toBeNull(); + }); +}); diff --git a/vite.config.ts b/vite.config.ts index bff385c..a6e5a4d 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.2", + version: "1.0.3", description: "Voice dictation for OpenCode web using Whisper (Groq API) - works on PC and mobile", author: "slaid098",