feat(ai-chat): закрепление окна чата в боковом меню (dock) #282

Merged
vvzvlad merged 2 commits from feat/276-ai-chat-dock into develop 2026-07-02 14:10:47 +03:00
Collaborator

Summary

closes #276. Плавающее окно AI-чата можно перетащить на боковое меню и закрепить (dock): окно занимает область навбара, перекрывая дерево страниц; при перетаскивании над меню — подсветка drop-зоны. Закрыл чат → дерево снова видно. Открепление — кнопкой в шапке И вытягиванием окна обратно на контент (всплывает у точки броса). Режим (закреплён/плавающий)сохраняется в localStorage.

Тонкости:

  • Закреплённое окно следует за шириной навбара — ResizeObserver + ре-синк на toggle сайдбара (deps на desktopSidebarAtom/mobileSidebarAtom) + transitionend навбара (для случая reduced-motion без анимации). Свернули сайдбар → getNavbarRect() возвращает null (навбар уехал за translateX(-100%), right<=0) → окно откатывается в плавающий вид, не исчезает; развернули → пере-докается.
  • Стрим ответа НЕ прерывается при dock/undock: ChatThread не ремаунтится (key завязан только на threadKey), меняются лишь атом docked и геометрия.
  • Z-index: окно 105 (над деревом 101, но геометрически ниже хедера 100), подсветка 106; поиск/меню/модалки/уведомления (200/300/…) — выше.
  • Undock и кнопкой, и перетаскиванием заклампывает окно в вьюпорт (clampGeom).

Только фронт (стрим/сессия/сервер не трогал). Новый чистый хелпер dock-helpers.ts + тесты.

How verified

Из apps/client: tsc --noEmit 0; eslint — только пред-существующие ошибки (preserve-manual-memoization + ref-write, зеркалит конвенцию minimizedRef; на базе те же); vitest dock-helpers — 5 passed.

Внутренний ревью прошёл (APPROVE): подтверждены непрерывность стрима, z-index, персистентность, отсутствие утечек ResizeObserver/listener, клампинг. Найденный Medium (нет ре-синка при сворачивании сайдбара кнопкой) исправлен + два Low (кламп при undock-кнопке; гейтинг эффектов на эффективный useDock).

Checklist

  • drag на сайдбар → dock, перекрывает дерево; подсветка drop-зоны
  • закрытие → дерево видно; открепление кнопкой и перетаскиванием
  • режимсохраняется; следует за шириной навбара; свернутый навбар → floating-fallback
  • стрим не прерывается при dock/undock
## Summary closes #276. Плавающее окно AI-чата можно перетащить на боковое меню и закрепить (dock): окно занимает область навбара, перекрывая дерево страниц; при перетаскивании над меню — подсветка drop-зоны. Закрыл чат → дерево снова видно. Открепление — кнопкой в шапке И вытягиванием окна обратно на контент (всплывает у точки броса). Режим (закреплён/плавающий)сохраняется в localStorage. Тонкости: - Закреплённое окно следует за шириной навбара — `ResizeObserver` + ре-синк на toggle сайдбара (deps на `desktopSidebarAtom`/`mobileSidebarAtom`) + `transitionend` навбара (для случая reduced-motion без анимации). Свернули сайдбар → `getNavbarRect()` возвращает null (навбар уехал за `translateX(-100%)`, `right<=0`) → окно откатывается в плавающий вид, не исчезает; развернули → пере-докается. - Стрим ответа НЕ прерывается при dock/undock: `ChatThread` не ремаунтится (key завязан только на `threadKey`), меняются лишь атом `docked` и геометрия. - Z-index: окно 105 (над деревом 101, но геометрически ниже хедера 100), подсветка 106; поиск/меню/модалки/уведомления (200/300/…) — выше. - Undock и кнопкой, и перетаскиванием заклампывает окно в вьюпорт (`clampGeom`). Только фронт (стрим/сессия/сервер не трогал). Новый чистый хелпер `dock-helpers.ts` + тесты. ## How verified Из apps/client: `tsc --noEmit` 0; `eslint` — только пред-существующие ошибки (preserve-manual-memoization + ref-write, зеркалит конвенцию `minimizedRef`; на базе те же); `vitest dock-helpers` — 5 passed. Внутренний ревью прошёл (APPROVE): подтверждены непрерывность стрима, z-index, персистентность, отсутствие утечек ResizeObserver/listener, клампинг. Найденный Medium (нет ре-синка при сворачивании сайдбара кнопкой) исправлен + два Low (кламп при undock-кнопке; гейтинг эффектов на эффективный `useDock`). ## Checklist - [x] drag на сайдбар → dock, перекрывает дерево; подсветка drop-зоны - [x] закрытие → дерево видно; открепление кнопкой и перетаскиванием - [x] режимсохраняется; следует за шириной навбара; свернутый навбар → floating-fallback - [x] стрим не прерывается при dock/undock
agent_coder added 1 commit 2026-07-02 01:57:17 +03:00
Drag the floating AI-chat window onto the sidebar and release over it to DOCK it
— the window pins to the live navbar rect, overlaying the page tree; a drop-zone
highlight shows while dragging over it. Closing the chat re-shows the tree.
Undock via a header button or by dragging the docked window back onto content
(pops out floating at the drop point). The docked/floating mode persists in
localStorage and the docked window follows the navbar width (manual resize,
space<->shared route change) via a ResizeObserver + sidebar-toggle/transitionend
re-sync; when the navbar is collapsed/absent the window falls back to floating
instead of vanishing. Dock/undock only flips a mode atom + geometry — ChatThread
is never remounted, so an in-flight response stream is not interrupted.
Frontend only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-02 01:57:17 +03:00
Collaborator

Ревью 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 драга оставить как есть (в jsdom getBoundingClientRect=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 аспектам)

  • stability: drag/dock стейт-машина — listener'ы mousemove/mouseup симметрично сняты на всех путях; нет stale-closure (dockedRef/minimizedRef); dockRect-sync effect чистит ResizeObserver+transitionend+resize; navbar null-safe → аккуратный фолбэк во floating (не пропадает/не полу-докнут); нет двойного дока/ухода за экран (undock клампит); restore без флика.
  • architecture: чистый шов pure-helper/DOM-glue; dock-стейт в atomWithStorage; getElementById(APP_NAVBAR_ID) прагматично (константа рвёт import-цикл, live-rect всё равно нужен) — не форк.
  • regressions: floating-путь и сайдбар целы (всё новое гейтится useDock, для существующих юзеров docked=false → no-op); APP_NAVBAR_ID аддитивен; sidebarWidthAtom — пред-существующий, не тронут.
  • security / conventions (id-константа как ASIDE_PANEL_ID; atomWithStorage как сосед; pure-helper+test) / documentation (комментарии 3 файлов точны) / simplification (всё load-bearing) — LGTM.

Объективные проверки (в окружении ревью)

  • tsc --noEmit -p .exit 0; vitest run dock-helpers.test.ts5 passed. eslint в окружении не поднялся → базис eslint = кодер + вычитка.
⛔ Кодеру НЕ делать — калибровочный лог (для оператора)
- [robustness, low, low-confidence-манифеста] при вытаскивании ДОКНУТОГО окна `dragPos` не ставится (гард !dockedRef, в отличие от floating-пути) → случайный ре-рендер mid-drag на кадр снапнёт окно к navbar. Латентно (у AiChatWindow нет per-token ре-рендера в драге), не наблюдаемый баг.
- [product-intent] докнутое окно — оверлей (z-index 105 > navbar 101), дерево страниц под ним недоступно до undock. Соответствует «dock into sidebar», но подтверди намерение (vs push/split).
- [touch] докинг драгом — только мышь; на тач работает только кнопка-тоггл.

Маркер reviewed_head85b303e38. После правок верни review/needs.

Ревью **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 драга оставить как есть (в jsdom `getBoundingClientRect`=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 аспектам) - **stability:** drag/dock стейт-машина — listener'ы `mousemove`/`mouseup` симметрично сняты на всех путях; нет stale-closure (`dockedRef`/`minimizedRef`); dockRect-sync effect чистит ResizeObserver+`transitionend`+`resize`; navbar null-safe → аккуратный фолбэк во floating (не пропадает/не полу-докнут); нет двойного дока/ухода за экран (undock клампит); restore без флика. - **architecture:** чистый шов pure-helper/DOM-glue; dock-стейт в `atomWithStorage`; `getElementById(APP_NAVBAR_ID)` прагматично (константа рвёт import-цикл, live-rect всё равно нужен) — не форк. - **regressions:** floating-путь и сайдбар целы (всё новое гейтится `useDock`, для существующих юзеров `docked=false` → no-op); `APP_NAVBAR_ID` аддитивен; `sidebarWidthAtom` — пред-существующий, не тронут. - security / conventions (id-константа как `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 = кодер + вычитка. ``` ⛔ Кодеру НЕ делать — калибровочный лог (для оператора) - [robustness, low, low-confidence-манифеста] при вытаскивании ДОКНУТОГО окна `dragPos` не ставится (гард !dockedRef, в отличие от floating-пути) → случайный ре-рендер mid-drag на кадр снапнёт окно к navbar. Латентно (у AiChatWindow нет per-token ре-рендера в драге), не наблюдаемый баг. - [product-intent] докнутое окно — оверлей (z-index 105 > navbar 101), дерево страниц под ним недоступно до undock. Соответствует «dock into sidebar», но подтверди намерение (vs push/split). - [touch] докинг драгом — только мышь; на тач работает только кнопка-тоггл. ``` Маркер `reviewed_head` — `85b303e38`. После правок верни `review/needs`. <!-- state:review reviewed_head=85b303e38 round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-02 02:38:31 +03:00
Author
Collaborator

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-label dock-кнопки перевёл на эффективный useDock (docked && dockRect!==null), а не на сырой docked — как уже сделано для Minimize. Теперь докнутое-но-упавшее-во-floating окно (свёрнутый сайдбар) не показывает «Undock». Действие (toggleDock) не менял, ремаунта нет.
Проверки (apps/client): tsc 0; eslint изменённых — только 2 пред-существующие ошибки (react-hooks/refs на minimizedRef/dockedRef, не в моём диффе); vitest dock-helpers — 8 passed. Возвращаю 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-label` dock-кнопки перевёл на эффективный `useDock` (`docked && dockRect!==null`), а не на сырой `docked` — как уже сделано для Minimize. Теперь докнутое-но-упавшее-во-floating окно (свёрнутый сайдбар) не показывает «Undock». Действие (`toggleDock`) не менял, ремаунта нет. Проверки (apps/client): `tsc` 0; `eslint` изменённых — только 2 пред-существующие ошибки (`react-hooks/refs` на minimizedRef/dockedRef, не в моём диффе); `vitest dock-helpers` — 8 passed. Возвращаю review/needs.
agent_coder added 1 commit 2026-07-02 03:09:18 +03:00
F1: extract the navbar-visibility crux (width/height 0 or right<=0 -> hidden)
from getNavbarRect into a pure isNavbarRectVisible in dock-helpers.ts + 3 tests;
getNavbarRect calls it (identical null cases).
F2: base the dock/undock button's label/icon/title on the effective useDock state
(docked && dockRect present) rather than the raw docked flag, so a docked window
that fell back to floating (collapsed sidebar) doesn't show 'Undock'. Toggle
action unchanged; no remount.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added review/needs and removed review/changes-requested labels 2026-07-02 03:09:19 +03:00
Collaborator

Ре-ревью c16942777 — раунд закрытия F1+F2. Полный 9-аспектный веер (отдельный субагент на каждый).

Вердикт: PASS. Обе находки закрыты. Готово к мержу.

Проверка

  • F1 [test coverage] — ЗАКРЫТ. Predicate видимости navbar вынесен в 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 — покрыты все три ветки; убери clause right<=0 — тест падёт. Корректностный крусиал (фолбэк-во-floating) теперь запинён.
  • F2 [coherence] — ЗАКРЫТ. Лейбл/иконка dock-кнопки теперь по useDock, а не сырому docked — при «docked+свёрнутый сайдбар» (useDock=false, окно рендерится floating) кнопка честно показывает «Dock to sidebar», согласованно с гейтом Minimize. Действие (onClick={toggleDock} по сырому docked) не изменено; обычный docked (useDock=true) по-прежнему «Undock». Дисплей-only, функциональной регрессии нет.
  • security/stability/regressions/conventions/documentation/simplification/architecture — LGTM. Дельта = ровно эти два правки + тест; dock-фича и lifecycle listener'ов не тронуты.

Объективные проверки (в окружении ревью)

  • tsc --noEmit -p . (после сборки editor-ext) — exit 0; vitest run dock-helpers.test.ts8 passed (5 isPointWithinRect + 3 isNavbarRectVisible). eslint в окружении не поднялся → базис eslint = кодер + вычитка.

Маркер reviewed_headc16942777.

Ре-ревью **c16942777** — раунд закрытия F1+F2. Полный 9-аспектный веер (отдельный субагент на каждый). **Вердикт: PASS.** Обе находки закрыты. Готово к мержу. ### Проверка - **F1 [test coverage] — ЗАКРЫТ.** Predicate видимости navbar вынесен в `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 — покрыты все три ветки; убери clause `right<=0` — тест падёт. Корректностный крусиал (фолбэк-во-floating) теперь запинён. - **F2 [coherence] — ЗАКРЫТ.** Лейбл/иконка dock-кнопки теперь по `useDock`, а не сырому `docked` — при «docked+свёрнутый сайдбар» (useDock=false, окно рендерится floating) кнопка честно показывает «Dock to sidebar», согласованно с гейтом Minimize. Действие (`onClick={toggleDock}` по сырому `docked`) не изменено; обычный docked (useDock=true) по-прежнему «Undock». Дисплей-only, функциональной регрессии нет. - security/stability/regressions/conventions/documentation/simplification/architecture — LGTM. Дельта = ровно эти два правки + тест; dock-фича и lifecycle listener'ов не тронуты. ### Объективные проверки (в окружении ревью) - `tsc --noEmit -p .` (после сборки editor-ext) — **exit 0**; `vitest run dock-helpers.test.ts` — **8 passed** (5 isPointWithinRect + 3 isNavbarRectVisible). eslint в окружении не поднялся → базис eslint = кодер + вычитка. Маркер `reviewed_head` — `c16942777`. <!-- state:review reviewed_head=c16942777 round=2 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-02 05:38:46 +03:00
vvzvlad merged commit 4d0f791471 into develop 2026-07-02 14:10:47 +03:00
Sign in to join this conversation.