feat(ai-chat): закрепление окна чата в боковом меню (dock) #282
Reference in New Issue
Block a user
Delete Branch "feat/276-ai-chat-dock"
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
closes #276. Плавающее окно AI-чата можно перетащить на боковое меню и закрепить (dock): окно занимает область навбара, перекрывая дерево страниц; при перетаскивании над меню — подсветка drop-зоны. Закрыл чат → дерево снова видно. Открепление — кнопкой в шапке И вытягиванием окна обратно на контент (всплывает у точки броса). Режим (закреплён/плавающий)сохраняется в localStorage.
Тонкости:
ResizeObserver+ ре-синк на toggle сайдбара (deps наdesktopSidebarAtom/mobileSidebarAtom) +transitionendнавбара (для случая reduced-motion без анимации). Свернули сайдбар →getNavbarRect()возвращает null (навбар уехал заtranslateX(-100%),right<=0) → окно откатывается в плавающий вид, не исчезает; развернули → пере-докается.ChatThreadне ремаунтится (key завязан только наthreadKey), меняются лишь атомdockedи геометрия.clampGeom).Только фронт (стрим/сессия/сервер не трогал). Новый чистый хелпер
dock-helpers.ts+ тесты.How verified
Из apps/client:
tsc --noEmit0;eslint— только пред-существующие ошибки (preserve-manual-memoization + ref-write, зеркалит конвенциюminimizedRef; на базе те же);vitest dock-helpers— 5 passed.Внутренний ревью прошёл (APPROVE): подтверждены непрерывность стрима, z-index, персистентность, отсутствие утечек ResizeObserver/listener, клампинг. Найденный Medium (нет ре-синка при сворачивании сайдбара кнопкой) исправлен + два Low (кламп при undock-кнопке; гейтинг эффектов на эффективный
useDock).Checklist
Ревью
85b303e38— докинг окна AI-чата в сайдбар (closes #276). Полный 9-аспектный веер (отдельный субагент на каждый).Вердикт: CHANGES. Фича инженерно крепкая (lifecycle listener'ов чистый, регрессий во floating-путь нет, чистая геометрия вынесена и покрыта) — но полный проход нашёл два дешёвых пункта: непокрытый predicate-крусиал и мелкую нестыковку лейбла. Оба low. Отвечай по id.
Что сделать
F1 [test coverage] Вынести и покрыть тестом predicate видимости navbar — это корректностный крусиал дока —
ai-chat-window.tsx:~187(getNavbarRect)if (r.width === 0 || r.height === 0 || r.right <= 0) return null;— ровно это решает «докнутое окно при свёрнутом/уехавшем navbar падает во floating, а не пинится в невидимый бокс». Ты уже вынес сестринскую чистую геометриюisPointWithinRectвdock-helpers.tsи покрыл её — этот predicate ровно того же рода, но остался вшит в DOM-чтение и без теста.Fix: вынести в
dock-helpers.tsкакisNavbarRectVisible({width,height,right})(илиvisibleRectOrNull),getNavbarRectзовёт его, + ~3 теста (zero-size→скрыт,right<=0→off-screen-скрыт, нормальный→виден). DOM-glue драга оставить как есть (в jsdomgetBoundingClientRect=0, юнит-тест бессмыслен, e2e в репо нет — это приемлемо).F2 [coherence] Лейбл dock-кнопки при «docked + свёрнутый сайдбар» рассинхронен —
ai-chat-window.tsx(dock-кнопка)Когда
docked===true, но сайдбар свёрнут (useDock===false), окно рендерится floating и возвращается кнопка Minimize (гейт!useDock), а dock-кнопка всё ещё читаетUndock(по сыромуdocked). Юзер видит плавающее окно с кнопкой «Undock» — сбивает.Fix: базировать лейбл/действие dock-кнопки на
useDock, а не на сыромdocked(как уже сделано для Minimize).Подтверждено чистым (по 9 аспектам)
mousemove/mouseupсимметрично сняты на всех путях; нет stale-closure (dockedRef/minimizedRef); dockRect-sync effect чистит ResizeObserver+transitionend+resize; navbar null-safe → аккуратный фолбэк во floating (не пропадает/не полу-докнут); нет двойного дока/ухода за экран (undock клампит); restore без флика.atomWithStorage;getElementById(APP_NAVBAR_ID)прагматично (константа рвёт import-цикл, live-rect всё равно нужен) — не форк.useDock, для существующих юзеровdocked=false→ no-op);APP_NAVBAR_IDаддитивен;sidebarWidthAtom— пред-существующий, не тронут.ASIDE_PANEL_ID;atomWithStorageкак сосед; pure-helper+test) / documentation (комментарии 3 файлов точны) / simplification (всё load-bearing) — LGTM.Объективные проверки (в окружении ревью)
tsc --noEmit -p .— exit 0;vitest run dock-helpers.test.ts— 5 passed. eslint в окружении не поднялся → базис eslint = кодер + вычитка.Маркер
reviewed_head—85b303e38. После правок верниreview/needs.F1: fixed — вынес predicate видимости навбара из
getNavbarRectв чистыйisNavbarRectVisible({width,height,right})вdock-helpers.ts(рядом сisPointWithinRect),getNavbarRectтеперь зовёт его — null-кейсы идентичны, поведение не менялось. +3 теста: normal→visible; width/height 0→скрыт; right===0 и right<0→скрыт. DOM-glue самого getNavbarRect не тестирую (в jsdom rect=0, e2e в репо нет — приемлемо).F2: fixed — лейбл/иконку/
title/aria-labeldock-кнопки перевёл на эффективныйuseDock(docked && dockRect!==null), а не на сыройdocked— как уже сделано для Minimize. Теперь докнутое-но-упавшее-во-floating окно (свёрнутый сайдбар) не показывает «Undock». Действие (toggleDock) не менял, ремаунта нет.Проверки (apps/client):
tsc0;eslintизменённых — только 2 пред-существующие ошибки (react-hooks/refsна minimizedRef/dockedRef, не в моём диффе);vitest dock-helpers— 8 passed. Возвращаю review/needs.Ре-ревью
c16942777— раунд закрытия F1+F2. Полный 9-аспектный веер (отдельный субагент на каждый).Вердикт: PASS. Обе находки закрыты. Готово к мержу.
Проверка
dock-helpers.tsкак чистыйisNavbarRectVisible(r) = !(width===0||height===0||right<=0),getNavbarRectзовёт его. Экстракция поведение-идентична (!isNavbarRectVisible= прежний инлайн-check). +3 non-vacuous теста: нормальный→true, zero-size (width ИЛИ height 0)→false, off-screen (right<=0, и 0, и отрицательный)→false — покрыты все три ветки; убери clauseright<=0— тест падёт. Корректностный крусиал (фолбэк-во-floating) теперь запинён.useDock, а не сыромуdocked— при «docked+свёрнутый сайдбар» (useDock=false, окно рендерится floating) кнопка честно показывает «Dock to sidebar», согласованно с гейтом Minimize. Действие (onClick={toggleDock}по сыромуdocked) не изменено; обычный docked (useDock=true) по-прежнему «Undock». Дисплей-only, функциональной регрессии нет.Объективные проверки (в окружении ревью)
tsc --noEmit -p .(после сборки editor-ext) — exit 0;vitest run dock-helpers.test.ts— 8 passed (5 isPointWithinRect + 3 isNavbarRectVisible). eslint в окружении не поднялся → базис eslint = кодер + вычитка.Маркер
reviewed_head—c16942777.