feat(dictation): reason-модель — говорящий tooltip на серой иконке + общий резолвер ошибок (#309) #314
Reference in New Issue
Block a user
Delete Branch "feat/309-dictation-reasons"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Микрофон диктовки мог быть серым/disabled и при этом молча показывать «Start dictation», а нативный Mantine
disabledставитpointer-events:none— тултип вообще не срабатывал: UI знал причину, но пользователю не сообщал. Плюс строки рантайм-ошибок были продублированы в двух хуках диктовки.dictation-status.ts— единый источник правды: enumDictationUnavailableReason(connecting/offline/read-only/unsupported/busy) + enumDictationErrorCode, чистые классификаторы и резолверы. Весь пользовательский текст диктовки рождается здесь; verbatim серверное сообщение по-прежнему побеждает для ошибок транскрипции.page-editorпубликуетdictationAvailabilityAtom { isEditable, reason }, вычисляя причину у источника (editable/edit-mode/showStatic/collab-статус):connectingvs залипшееofflinevsread-only.DictationGroupпрокидывает reason вMicButton.MicButtonreason-aware: серый микрофон показывает причинно-специфичный тултип. Молчание тултипа на disabled чинится «по-мантайновски» (data-disabled/aria-disabled+ guard клика) вместо нативного атрибута — и в idle-ветке (reason), и в error-ветке (errorMessage).transcriptionErrorMessage), формулировки побайтово совпадают с оригиналом каждого хука (включая name-префикс batch-хука и verbatim серверное сообщение).closes #309
How verified
apps/clientvitest (dictation+editor-sync-state) → 5 files / 32 passed.dictation-status.test.tsпокрывает все ветки классификаторов/резолверов (incl. verbatim server passthrough);mic-button.test.tsx— реальный рендер (MantineProvider): disabled-микрофон сunavailableReason="offline"показывает текст причины (не «Start dictation»),data-disabledвместо нативногоdisabled; падает против дореформенного кода.tsc --noEmit— чисто по затронутым файлам; eslint — чисто по новым/изменённым (предсуществующиеreact-hooks/refsв use-dictation/page-editor не трогал).data-disabled), и batch-хук вернулname-префикс в тексте unknown-ошибки (побайтовое совпадение с оригиналом).Checklist
Ревью — #314 (dictation: reason-модель — говорящий tooltip + общий error-resolver, #309), round 1, head
e808df86, base developScope: реальная дельта — ТОЛЬКО
e808df86поверх смердженного #304 (родитель88d96c41); 11 файлов, ~530+/88− (НОВЫЙ dictation-status.ts +резолверы+тест, mic-button, 2 хука-рефактор, editor-atoms, dictation-group, page-editor, en+ru локали). #304 не ревьюился — предок.Вердикт: CHANGES — фича сделана добротно (data-flow до говорящего tooltip'а целен, рефактор строк байт-идентичен, объективка зелёная), но два in-scope DO: (1) ключевая новая логика (какую причину показать) без тестов + дублирует уже-тестированный гейт; (2) самонесогласованность —
disabledберётся из НЕреактивного источника, хотя PR уже публикует реактивный. Оба — маленькие, приближают к цели + чисто сходятся с #316.Полный веер 7 аспектов. Объективка запущена мной (детач
e808df86): clienttsc --noEmit→ 0;vitest dictation-status + mic-button→ 2 files, 15 passed.Do — примени, затем ре-ревью
page-editor.tsx:477-498(новый publish-useEffect). ВычислениеdictationAvailability.reason(offline vs connecting vs read-only) — САМЫЙ рискованный новый путь (решает, какую причину видит юзер, — вся суть #309) — идёт с НУЛЁМ тестов, тогда как строки-резолверы, что он кормит (dictation-status), покрыты полностью (покрытие инвертировано). Плюс:482 const isEditable = editable && inEditMode && !showStaticРУЧНО дублируетisBodyEditable({editable,inEditMode,showStatic})— экспортируемый тестированный хелпер (editor-sync-state.ts:26,editor-sync-state.test.ts), который ЭТОТ ЖЕ файл уже импортит и зовёт на:469(гейт #218). Копия молча разойдётся с гейтом, если тот изменится → tooltip соврёт. Fix: вынести чистый хелпер (напр. вeditor-sync-state.ts, ПЕРЕИСПОЛЬЗУЯisBodyEditable), напр.computeDictationAvailability({editable,inEditMode,showStatic,yjsStatus}) → {isEditable, reason}; useEffect → однострочный вызов + setDictationAvailability; юнит-тест по образцуeditor-sync-state.test.tsна ветки (editable+edit+synced→null; +showStatic+Disconnected→offline; +Connecting→connecting; !editable/!edit→read-only).disabledиз нереактивного источника при уже-опубликованном реактивном] —dictation-group.tsx:86. PR вычисляет и публикуетdictationAvailability.isEditable— РЕАКТИВНОЕ зеркалоeditor.isEditable(те же сигналы, подтверждено) — но потребляет только.reason, аdisabled={!editor.isEditable}читает НЕреактивное tiptap-поле. Так гейт и его причина едут от РАЗНЫХ источников: протухшее поле для гейта, живой atom для объяснения → мик может застрять в неверном состоянии до несвязанного ре-рендера. Fix:disabled={!dictationAvailability.isEditable}— один источник истины для гейта И причины, и (atom реактивен) убирает stuck-mic и в #314-standalone. Значение идентичноeditor.isEditableв steady-state, поведение не меняется кроме обретённой реактивности (transient: initial{isEditable:false}кратко дизейблит на mount до publish-эффекта — самокорректируется за тик).Подтверждено по коду + прогоны (не блокирует)
{isEditable,reason}→ atom → dictation-group форвардитunavailableReason→ MicButton резолвитresolveUnavailableLabelв tooltip+aria-label.unsupportedдетектится локально в MicButton и имеет приоритет. Серый мик теперь говорит причину. Единый источник строк (dictation-status.ts) — выполнено.Could not start recording: <name>: <detail>, mapped denied/no-mic/in-use, recorder-failed, transcription serverMsg-verbatim/503|403→not-configured/Transcription failed: <detail>, streaming VAD unknown БЕЗ name-префикса как и было) воспроизведены точно. notifications.show не задублирован/не потерян; +setErrorMessage тем же текстом.data-disabled/aria-disabledвместо нативногоdisabled(нативный → pointer-events:none → убил бы tooltip); клик гвардится (isDisabled→preventDefault); enabled-клик стартует; recording/loading/error не сломаны. Паттерн уже есть (space-row/page-row).[drainResults,t]), хук-ордеринг цел, isDictationSupported SSR-safe, page-editor-эффект без петли. Reason-классификация не «врёт» (!editableкороткозамыкает в read-only ДО offline/connecting-ветки).Заметка (для vvzvlad — НЕ блокер, НЕ эскалация) — взаимодействие с #316
#314 и #316 (уже approved) ОБА правят
dictation-group.tsx. #316 меняетdisabled={!editor.isEditable}→ реактивноеuseEditorState-чтение (фикс stuck-mic). #314 держит нереактивное + добавляетunavailableReason+импорт. При мердже — ТЕКСТОВЫЙ конфликт (та же строка/импорты) + #314 as-is несёт stuck-mic-баг, что #316 чинит. Оба нужны. F2 выше делает это чисто сходящимся: если #314 берётdisabled={!dictationAvailability.isEditable}, строка становится реактивной И несёт причину — намерение #316 уже выполнено, и человеку/кодеру на rebase остаётся «оставить строку #314, правку #316 дропнуть как избыточную» (две реактивные механики — jotai-atom #314 vs useEditorState #316 — atom достаточно). Агент-разрешимо, не design-fork.⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора)
[below-threshold]low/med[test-coverage] хукиerrorMessage-state (clear-on-start + set-on-catch) напрямую не тестится — но КОНТЕНТ каждого сообщения полностью покрыт dictation-status.test (хуки лишь зовут тестированные резолверы + кладут в useState); wiring — простой pass-through, тест дороже (гонять getUserMedia/VAD/transcribe-моки). Ниже порога.[out-of-scope]low/high[documentation/i18n]t("Preparing…")(mic-button loading-ветка) нет ни в en ни в ru → ru видит англ. Пред-существующее (ключ лишь реструктурирован, не введён этим PR), не ключ-резолвер → не DO этого PR.Починил F1 и F2 (коммит
c9668cbe).F1: fixed — вынес чистый
computeDictationAvailability({editable,inEditMode,showStatic,isDisconnected}) → {isEditable, reason}вeditor-sync-state.ts, ПЕРЕИСПОЛЬЗУЯisBodyEditable(ту же тестированную функцию, что и гейт #218 на:469) — ручной дубликатeditable && inEditMode && !showStaticубран, разойтись с гейтом больше нечему. Publish-эффект вpage-editor.tsxстал однострочным вызовом. Юнит-тесты вeditor-sync-state.test.ts(по образцу тестовisBodyEditable): synced→{true,null}; pre-sync disconnected→offline; pre-sync connecting→connecting;!editable→read-only;!inEditMode→read-only.F2: fixed —
dictation-group.tsx:86теперьdisabled={!dictationAvailability.isEditable}вместо нереактивного!editor.isEditable. Гейт и причина едут из ОДНОГО реактивного источника (atom, что компонент уже читает для.reason). В steady-state значение идентичноeditor.isEditable, но теперь реактивно — заодно снимает stuck-mic и в этом PR standalone.useEditorStateне добавлял (это отдельный механизм #316; atom достаточно).По твоей заметке о пересечении с #316: как ты и указал, F2 делает мердж чисто сходящимся — строка
dictation-groupв #314 теперь реактивна через atom, так что на rebase правку #316 (useEditorState) можно дропнуть как избыточную.vitest(editor-sync-state + dictation) → 37 passed; tsc/eslint по затронутым чисто (предсуществующие react-hooks/refs в page-editor не трогал).DROP-пункты (errorMessage-wiring тест,
Preparing…i18n) — как помечено, не трогал.Ре-ревью — #314 (dictation reason-модель, #309), round 2, head
c9668cbe, base developДельта с моего r1-marker
e808df865d: 4 файла (dictation-group.tsx +1/-1, editor-sync-state.ts +30, .test +54, page-editor.tsx −19/+11) — фикс F1/F2.Вердикт: PASS — оба round-1 замечания закрыты по-настоящему (сверено по коду), объективка зелёная. Готово к мержу.
Целевая проверка (regressions+coherence, test-coverage) + моя сверка. Объективка запущена мной (детач
c9668cbe): clienttsc --noEmit→ 0;vitest editor-sync-state + dictation-status + mic-button→ 3 files, 25 passed.Закрыто (сверено по коду + прогоны)
computeDictationAvailability({editable,inEditMode,showStatic,isDisconnected})→{isEditable,reason}вeditor-sync-state.ts:44, ПЕРЕИСПОЛЬЗУЯisBodyEditable(тот же тестированный гейт #218) — ручной дубликатeditable&&inEditMode&&!showStaticубран, разойтись с гейтом нечему. Модуль остаётся свободен от collab-enum (принимаетisDisconnectedboolean). Publish-эффект в page-editor стал однострочным вызовом; устаревшийDictationUnavailableReason-импорт оттуда убран (grep — ноль остаточных). Precedence воспроизведена ТОЧНО (r1-поведение сохранено). 5 branch-тестов НЕ-вакуозны (toEqualна полный объект): synced→{true,null}; pre-sync+disconnected→offline; pre-sync+connecting→connecting; !editable→read-only; !inEditMode→read-only. offline/connecting-развилка (легче всего регрессит) запиннена тестами 2/3 (различаются только isDisconnected); обе read-only-ветки покрыты раздельно. Все ветки покрыты.dictation-group.tsx:86теперьdisabled={!dictationAvailability.isEditable}(реактивный atom, что компонент уже читает для.reason) вместо нереактивного!editor.isEditable. Гейт и причина — из ОДНОГО реактивного источника; steady-state значение идентично, но реактивно (снимает stuck-mic и в #314 standalone).!editable/!inEditMode→read-only короткозамыкает ДО offline/connecting). Как я отмечал: F2 делает мердж с #316 чисто сходящимся (строка dictation-group теперь реактивна через atom → правку #316useEditorStateна rebase можно дропнуть как избыточную).The dictation mic could be grey/disabled while silently showing "Start dictation", and Mantine's native `disabled` set pointer-events:none so the Tooltip never fired at all — the UI knew the cause but told the user nothing. Runtime error strings were also duplicated verbatim across the two dictation hooks. - New dictation-status.ts: the single source of truth. A DictationUnavailableReason enum (connecting/offline/read-only/unsupported/busy) + a DictationErrorCode enum, pure classifiers (classifyGetUserMediaError / classifyTranscriptionError) and resolvers (resolveUnavailableLabel / dictationErrorMessage). All user-facing dictation strings are formed here; the verbatim server message still wins for transcription errors. - page-editor publishes dictationAvailabilityAtom { isEditable, reason } computed at the source (editable/edit-mode/showStatic/collab status): connecting vs offline (stuck) vs read-only. DictationGroup forwards the reason to MicButton. - MicButton is reason-aware: a disabled mic shows the cause-specific tooltip. The disabled-hover silence is fixed by marking disabled the Mantine way (data-disabled/aria-disabled + click guard) instead of the native attribute, so the Tooltip fires — applied to both the idle (reason) and error (errorMessage) states. - Both hooks route every error through the shared resolver (deleting the duplicated transcriptionErrorMessage), and expose errorMessage for the tooltip. Wording is byte-identical to each hook's original (incl. the batch hook's DOMException name prefix and the verbatim server message). - i18n: 3 new reason keys in en-US + ru-RU, and the previously-missing ru-RU dictation error translations. Tests: dictation-status.test.ts (all classifier/resolver branches, incl. server message passthrough) + mic-button.test.tsx (disabled mic shows the reason text, uses data-disabled not native disabled — fails against the pre-fix code). vitest: 5 files / 32 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>c9668cbe60tod57392b5afОтребейзил на текущий develop (по твоей просьбе), новый head
d57392b5.Был конфликт в
dictation-group.tsx: пока #314 висел, смержился #316 (реактивное чтение черезuseEditorState), и оба PR правят одну и ту же строкуdisabled. Разрешил ровно так, как отметил ревьюер в round-1: оставил атомный вариант #314 (disabled={!dictationAvailability.isEditable}— единый реактивный источник и для гейта, и для причины), а правку #316 (useEditorState) выкинул как избыточную (двух реактивных механик не нужно, атома достаточно).Заодно привёл в соответствие унаследованный от #316 тест
dictation-group.test.tsx: он мокалuseEditorStateи гонялeditor.isEditable, что после дропа useEditorState ломало сьют (мокjotaiбезatom). Переписал его на атомный механизм через реальный jotai-store: флипdictationAvailabilityAtom.isEditablefalse→true → мик оживает. Покрытие #311 (иконка не залипает) сохранено, просто на новом (атомном) механизме.Прогон на ребейзнутой ветке:
vitestdictation + editor-sync + dictation-group → 38 passed (6 files); tsc по затронутым чисто.Ставлю
review/needs: ребейз-разрешение конфликта — это новый контент поверх одобренного head (дроп механики #316 + правка теста), так что нужен твой ре-взгляд, approved на старом head сам не переношу.Ре-ревью — #314 (dictation: reason-модель — говорящий tooltip + общий error-resolver, #309), round 3, head
d57392b5, base develop (после ребейза)Scope: ПОЛНЫЙ дифф всего PR
b861266f..d57392b5(14 файлов, +639, весь клиент-diff — не только дельта-инкремент). Ветка отребейжена на текущий develop (в нём уже смерджен #316). Прогнал полный веер 9 аспектов заново по всему PR.Вердикт: CHANGES — ребейз-резолв корректен и оба round-1 замечания закрыты по-настоящему (сверено по коду), объективка зелёная. Но полный веер по всему PR нашёл 3 маленьких in-scope DO: непокрытый precedence в ядре фичи, вводящий-в-заблуждение tooltip на chat-микрофоне (побочка перехода native-
disabled→data-disabled) и мёртвая reason-ветка"busy". Все — bounded, low.Объективка запущена мной (детач
d57392b5, main-клон): clientvitest(dictation-status + mic-button + dictation-group + editor-sync-state) → 4 files, 26 passed;tsc --noEmit→ 0.Закрыто (сверено по коду) — round-1 F1/F2 и ребейз-резолв
computeDictationAvailabilityвынесен вeditor-sync-state.ts, переиспользуетisBodyEditable(тот же тестированный гейт), ручной дубликат убран. Precedence воспроизведён точно.dictationAvailabilityAtom.isEditable, а не из нереактивногоeditor.isEditable.useEditorState) — КОРРЕКТЕН, без регрессий. ЕдинственныйsetEditableтела —page-editor.tsx:485черезisBodyEditable; новый publish-эффект (page-editor.tsx:497-511) считаетisEditableтой же формулой на супернаборе тех же сигналов (editable, currentPageEditMode, showStatic, yjsConnectionStatus) → атом трекаетeditor.isEditableна каждом переходе, jotai перерисовываетDictationGroupв том же commit'е — залипания серой иконки (#311/#316) НЕ возникает.useEditorStateдля диктовки больше нигде не ждётся (grep чист). Тестdictation-group.test.tsxпереписан на атомный механизм через реальный jotai-store (флипisEditablefalse→true оживляет мик) — не-вакуозен, покрытие #311 сохранено. Строки ошибок в обоих хуках байт-идентичны (verbatim серверное сообщение по-прежнему побеждает). Security/architecture/conventions/documentation — чисто.Do — примени, затем ре-ревью
editor-sync-state.ts:57(гард pre-sync) +editor-sync-state.test.ts. Ветка pre-sync (offline/connecting) охраняетсяopts.editable && opts.inEditMode && opts.showStatic, аread-only— это fallback (:61). Тестыread-onlyидут сshowStatic:false, а тестыoffline/connecting— сeditable:true, поэтому ПЕРЕСЕЧЕНИЕ (read-only-зритель В pre-sync-окне:editable:false, showStatic:true, isDisconnected:true) не запиннено НИЧЕМ. Мутант, уронившийopts.editable &&из гарда:57, переживёт весь сьют и покажет read-only-зрителю «connecting/offline» вместо «read-only» на этапе загрузки (read-only-зритель проходит ровно через это static/pre-sync-окно) — достижимая регрессия смысла (какую причину видит юзер = вся суть #309). Fix: добавить full-object-ассерт на precedence, напр.computeDictationAvailability({ editable:false, inEditMode:true, showStatic:true, isDisconnected:true })→{ isEditable:false, reason:"read-only" }(и вариантisDisconnected:falseтуда же).mic-button.tsx:133-155, потребительchat-input.tsx:79. PR заменил нативныйdisabledнаdata-disabled(чтобы tooltip срабатывал — цель фичи). Ноchat-inputпередаётdisabled={isStreaming || disabled}БЕЗunavailableReason, поэтому у mic-buttonreason===undefinedиidleLabelоткатывается кt("Start dictation"). Итог: пока стримится ответ ассистента, серый chat-микрофон теперь ховерится и показывает «Start dictation» на кнопке, клик по которой отвергается (onClickearly-return). До PR нативныйdisabledглушил и ховер, и клик. Функционально не ломается (клик защищён), но это новая UX-побочка на существующем виджете. Fix: вmic-button, когдаisDisabled && !reason, не показывать idle-лейбл «Start dictation» (не оборачивать в Tooltip / нейтральный лейбл) — чинит для любого потребителя, передающего голыйdisabled. (Не заводить reason="busy" наisStreaming: это НЕ «Transcribing», был бы неверный текст.)"busy"] —dictation-status.ts:10(член юниона) +:109-111(case "busy" → t("Transcribing…")) +dictation-status.test.ts:149(ассерт этой ветки)."busy"не производится НИКАКИМ путём:computeDictationAvailabilityдаёт толькоnull/offline/connecting/read-only, mic-button добавляет толькоunsupported. Ветка недостижима в проде, а тест пиннит мёртвое поведение (вакуозное покрытие). Fix: удалить член"busy"из юниона, егоcaseи строку теста:149(defaultуже даёт фолбэк).⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора)
[below-threshold]low/med[stability]page-editor.tsx:498computeDictationAvailabilityвозвращает свежий объект каждый прогон эффекта → новая ссылка атома на переходахyjsConnectionStatusмежду не-Disconnected значениями при идентичном{isEditable,reason}→ одна крошечная кнопка перерисовывается. Тривиальная цена, можно дедупить shallow-equal, но не обязательно.[below-threshold]low/low[stability]use-streaming-dictation.ts:192-202ставитsetErrorMessageна сбое сегмента, но НЕ ставитstatus="error"(только батч-хук ставит); mic-button показываетerrorMessageлишь в веткеstatus==="error", так что в стриминге сообщение до tooltip'а не доходит — но тост юзера всё равно информирует. Пре-существующая модель (у стриминга никогда не было error-статуса), не регрессия.Починил F3, F4, F5 (коммит
1458e3e1).F3: fixed — добавил в
editor-sync-state.test.tsассерты на пересечение read-only ∩ pre-sync (editable:false, inEditMode:true, showStatic:true) →{isEditable:false, reason:"read-only"}для обоихisDisconnected. Пиннит, что отсутствие права редактирования приоритетнее pre-sync-причины (убивает мутант, дропнувшийopts.editable &&из гарда — read-only-зритель на загрузке больше не увидит «connecting/offline»).F4: fixed — переход native
disabled→data-disabledсделал disabled-мик hoverable (нужно для байлайна, чтобы показать причину), но потребитель с голымdisabledбез reason (chat-input,isStreaming) получал вводящий в заблуждение «Start dictation» на кнопке, клик по которой отвергается. Теперь три ветки: disabled без reason → иконка БЕЗ Tooltip + нейтральный aria-label (t("Dictation")); disabled с reason (байлайн) → tooltip с причиной; enabled → «Start dictation». Click-guard/data-disabled/aria-disabledсохранены; reason="busy" наisStreamingне заводил (был бы неверный текст «Transcribing»). Чинит для ЛЮБОГО потребителя с голымdisabled. Тест на «не рекламирует Start dictation при disabled без reason» добавлен.F5: fixed — удалил мёртвую reason-ветку
"busy"(её не производит ни один путь): член юниона,case(свёрнут вdefault, резолвер остаётся total) и вакуозный ассерт теста. Grep поapps/client/src— ноль ссылок на"busy".vitest(dictation + editor-sync + dictation-group) → 41 passed; tsc/eslint по затронутым чисто.DROP-пункты (shallow-equal дедуп атома; streaming errorMessage не доходит до tooltip) — как помечено, пре-существующее/не регрессия, не трогал.
Ре-ревью — #314 (dictation: reason-модель, #309), round 4, head
1458e3e1, base developДельта r3→r4 (коммит
1458e3e1, F3/F4/F5): +тест precedence, переработка mic-button на 3 ветки tooltip'а, удаление мёртвой"busy". Полный веер 9 аспектов заново по всему PR (b861266f..1458e3e1, 14 файлов, +695).Вердикт: CHANGES — все три round-3 замечания закрыты по-настоящему (сверено по коду), объективка зелёная. Но сам фикс F4 занёс одну маленькую i18n-дырку: новый aria-label
t("Dictation")не заведён НИ в одну локаль → ru-RU-юзер (скринридер) слышит английское «Dictation». Один low DO.Объективка запущена мной (детач
1458e3e1, main-клон): clientvitest(dictation-status + mic-button + dictation-group + editor-sync-state) → 4 files, 29 passed;tsc --noEmit→ 0.Закрыто (сверено по коду + прогоны)
editor-sync-state.test.tsдва.toEqual({isEditable:false, reason:"read-only"})на{editable:false, inEditMode:true, showStatic:true}для обоихisDisconnected. Убивает мутанта, дропнувшегоopts.editable &&из pre-sync-гарда (editor-sync-state.ts): read-only-зритель на загрузке получил бы «offline/connecting» → тест бы упал. Приоритет «нет права редактирования > pre-sync» запиннен.data-disabled, hover работает — суть #309 цела); disabled БЕЗ reason → голая иконка без Tooltip + нейтральный aria; enabled → «Start dictation». Click-guard (if isDisabled preventDefault) цел во ВСЕХ ветках (и мышь, и клавиатура — контрол остаётся нативной<button>). Enabled-путь и disabled-with-reason (тулбар) байт-эквивалентны r3; изменился только chat-input-путь (голыйdisabledприisStreaming) — теперь без ложного «Start dictation». Recording/transcribing-ветки не тронуты."busy"] — ЗАКРЫТ."busy"удалён полностью (union →connecting|offline|read-only|unsupported,case, тест); grep поfeatures/dictation+features/editor— ноль остаточных ссылок.resolveUnavailableLabelтотален (default-ветка).Do — примени, затем ре-ревью
mic-button.tsx:143(ariaLabel = reasonLabel ?? (isDisabled ? t("Dictation") : t("Start dictation"))). Ключа"Dictation"НЕТ ни вen-US/translation.json, ни вru-RU/translation.json(есть только длинные «Dictation becomes available…»/«Dictation language»). Путь достижим в проде:isDisabled && !reasonLabel— это ровно как chat-input подключает мик (chat-input.tsx:79disabled={isStreaming || disabled}без reason), т.е. пока стримится ответ ассистента. Последствия: en-US — фолбэк на ключ «Dictation» (по счастью валидный английский), а ru-RU-юзер получает английское «Dictation» как accessible-label микрофона вместо «Диктовка»; плюс ключ не попадёт в Crowdin (в source-локали en-US его нет). Все прочие новые ключи этого PR заведены в ОБЕ локали — это единственный пропуск, занесённый фиксом F4. Severity low (только aria-label, не видимый текст), но реальная локализационная дырка. Fix: добавить"Dictation": "Dictation"в en-US и"Dictation": "Диктовка"в ru-RU (или переиспользовать существующий локализованный ключ).⛔ DROP — кодеру НЕ делать · калибровочный лог (для оператора)
[speculative]low/low[coherence/stability] ветка reason→tooltip кейсится наreasonLabel, не наisDisabled: гипотетический вызыватель, передавшийunavailableReasonприdisabled=false, показал бы tooltip причины на кликабельном мике. Ни один живой вызыватель так не делает (reason и disabled едут из одного атома, reason===null ⇔ editable) — латентно, не баг.[style/linter]low/low[conventions]mic-button.tsx:143— одна ~84-симв строка, тогда как файл переносит тернарники; Prettier сам переформатирует, косметика.[out-of-scope]low[conventions]t("Preparing…")(mic-button.tsx:101) тоже отсутствует в локалях, но это ПРЕ-существующая строка (не дельта этого PR) — не задача этого раунда.The F4 fix introduced t("Dictation") as the neutral aria-label for a disabled mic with no reason (reachable via the AI chat mic while the assistant streams), but the key wasn't in either locale — a ru-RU screen-reader user would hear the English "Dictation". Add it to both locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Починил F6 (коммит
b7c16dc6).F6: fixed — фикс F4 завёл
t("Dictation")как нейтральный aria-label для disabled-мика без причины (достижимо через chat-мик, пока стримится ответ), но ключа не было ни в одной локали → ru-RU-скринридер услышал бы английское «Dictation». Добавил"Dictation": "Dictation"в en-US и"Dictation": "Диктовка"в ru-RU (рядом с сиблинг-ключами диктовки). Оба JSON валидны.DROP-пункты (reason→tooltip кейсится на reasonLabel не на isDisabled — латентно, живых вызывателей нет; длина строки — Prettier;
Preparing…— пре-существующее) — как помечено, не трогал.Ре-ревью — #314 (dictation: reason-модель — говорящий tooltip + общий error-resolver, #309), round 5, head
b7c16dc6, base developДельта r4→r5 (коммит
b7c16dc6) — ровно 2 строки: добавлен i18n-ключ"Dictation"в en-US ("Dictation") и ru-RU ("Диктовка"), закрывает F6. Полный веер 9 аспектов заново по всему PR (b861266f..b7c16dc6).Вердикт: PASS — все замечания четырёх раундов закрыты по-настоящему (сверено по коду), объективка зелёная. Готово к мержу.
Объективка запущена мной (детач
b7c16dc6, main-клон): clientvitest(dictation-status + mic-button + dictation-group + editor-sync-state) → 4 files, 29 passed;tsc --noEmit→ 0.Закрыто (сверено по коду + прогоны)
"Dictation": "Dictation"(en-US:1277) +"Dictation": "Диктовка"(ru-RU:403). Оба JSON валидны, дубликатов нет, коллизии с длинными «Dictation …»-ключами нет (i18next — exact-match). Ключ резолвится в обеих локалях дляt("Dictation")(mic-button.tsx:143) — ru-RU-скринридер теперь слышит «Диктовка». Bilingual-паритет всего PR держится.computeDictationAvailability, мик гейтится из реактивного атома), F3 (precedence read-only∩pre-sync запиннен, мутант убит), F4 (mic-button 3 ветки, click-guard цел, суть #309 — говорящий tooltip на disabled — работает), F5 (мёртвая"busy"удалена начисто) — все закрыты. Веер 9 аспектов на r5 — чисто (security/stability/regressions/test-coverage/conventions/documentation/simplification/architecture/coherence LGTM). PR — нетто-упрощение (общий resolver, атом-в-источнике, без мёртвого кода).Готово к мержу.