## Что сделано Фикс бага из issue #32: в child session (subagent) кнопка 🎤 появлялась над disabled-блоком "Prompt is disabled / Back to parent", а клик падал с toast "Could not find input field". - **`src/ui.ts` `findComposer()`** — добавлен guard: `session-prompt-dock` skip'ается в цикле `COMPOSER_SELECTORS`, если внутри него НЕТ `[data-component="prompt-input"]` или `[data-component="prompt-input-v2"]`. В child session внутри dock'а только disabled-блок — guard срабатывает, `findComposer()` возвращает `null`, кнопка не вставляется. - **`src/ui.ts` `injectIntoComposer()`** — defence-in-depth: ранний return, если в документе нет `[data-component="prompt-input"], [data-component="prompt-input-v2"]`. Страховка от любых future-случаев отсутствия реального composer (не только child session). - **Бамп `@version`** 1.0.2 → 1.0.3 синхронно в `package.json` и `vite.config.ts` — разблокировать автообновление userscript-менеджеров (ловушка PR #29 → #30). - **`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). - **Handoff** `docs/handoff/pr-XX-child-session-disabled-composer.md` и **ADR 0004** `docs/decisions/0004-pr-XX-child-session-composer-guard.md` с placeholder'ом PR-номера (исправлю после получения номера). Это тот же класс бага, что и PR #31 (question-dock duplicate button) — `session-prompt-dock` использовался как composer-target когда реальный composer внутри не отрендерен. PR #31 пофиксил question-dock case, этот PR закрывает child-session case. ## Почему - **`session-prompt-dock` — общий wrapper для трёх состояний.** В `anomalyco/opencode` (`session-composer-region.tsx`) dock рендерится всегда когда `showComposer()` truthy (`!blocked() || !!parentID()` → в child session всегда truthy). Внутри условно: question-dock (PR #31), disabled-блок "Prompt is disabled / Back to parent" (child session, этот PR), или реальный composer. - **`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`) не срабатывал — внутри disabled-блок, не question-dock. - **Клик падал.** Кнопка вставлялась в dock через `injectIntoElement` (`position: absolute; top: 8px; right: 8px`). При клике → `insertIntoContenteditable()` → `querySelector('[data-component="prompt-input"]')` = `null` → toast "Could not find input field". - **Defence-in-depth.** Guard в `findComposer` (не возвращать dock без composer внутри) + guard в `injectIntoComposer` (не вставлять если в документе нет composer). Оба слоя независимы — любой один достаточно, второй страховка на race conditions. Аналог паттерна PR #31 / ADR 0003. Closes #32 Closes #32 --------- Co-authored-by: opencode-agent <agent@opencode.local>
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
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 =
|
|
'<div data-component="session-prompt-dock"><div data-component="prompt-input-v2"></div></div>';
|
|
|
|
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 =
|
|
'<div data-component="session-prompt-dock"><div data-component="prompt-input" contenteditable="true"></div></div>';
|
|
|
|
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 =
|
|
'<div data-component="session-prompt-dock"><div>Prompt is disabled</div><button type="button">Back to parent</button></div>';
|
|
|
|
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 = '<div data-component="session-prompt-dock"></div>';
|
|
|
|
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 =
|
|
'<div data-component="session-prompt-dock"><div data-component="session-question-dock"><textarea data-slot="question-custom-input"></textarea></div></div>';
|
|
|
|
setupUI(setupCallbacks());
|
|
|
|
const dock = document.querySelector('[data-component="session-prompt-dock"]');
|
|
const composerContainer = dock?.querySelector(":scope > .ocvd-container");
|
|
expect(composerContainer).toBeNull();
|
|
});
|
|
});
|