feat(ai-chat): getCurrentPage отдаёт текущее выделение пользователя (#388) #390
Reference in New Issue
Block a user
Delete Branch "feat/388-getcurrentpage-selection"
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
Тула
getCurrentPageтеперь отдаёт агенту не только страницу, но и текущее выделение пользователя в редакторе — чтобы «вот тут исправь» / «переформулируй это» имели адрес. closes #388Как реализовано (строго по принятым решениям issue):
get-editor-selection.tsснимает выделение из состояния редактора;chat-thread.tsxзовёт его через live-ref внутриprepareSendMessagesRequest— тот же паттерн, чтоopenPageRef. Каждый ход шлёт свежий снапшот, multi-turn работает сам, stale-closure нет.getCurrentPage, не отдельная тула. Нулевой рост каталога, тула остаётся core.openPage.resolveOpenPageContextfail-closed: нет id / страница не найдена / чужой воркспейс /validateCanViewбросил →return nullдля ВСЕГО контекста ещё до того, как выделение где-либо трогается.sanitizeSelectionзовётся только в финальном success-return. На недоступную/чужую/удалённую страницу выделение утечь не может, факт существования страницы не раскрывается.text, ниbefore/after, ниblockIdsв промпт не интерполируются (тип параметра сужен доobject | null). Текст — untrusted-контент, SAFETY_FRAMEWORK трактует его как данные.searchInPage/getNode) перед правкой.sanitizeSelection: не-объект/пустой text →null; text cap 4000 (+truncated);blockIds— только непустые строки ≤64 симв., срез до 20;before/aftercap 200; результат собирается в свежий объект (неизвестные поля отбрасываются). Клиент шлётblockIds+before/after, НЕ PMfrom/to.How verified
Прогнал на стенде из чистого состояния воркти (deps на месте):
jest ai-chat.prompt.spec ai-chat.service.spec ai-chat-tools.service.spec current-page.util.spec→ 164 passed, 4 suites.vitest chat-thread.test get-editor-selection.test→ 35 passed, 2 files (get-editor-selection.ts — 100% функций, 96.77% строк).tsc -p apps/server— чисто по всем файлам #388 (единственная ошибкаprom-clientmodule-resolution — предсуществующий шум, не из этого PR).Внутренний цикл: 1 проход ревью (general-purpose sub-agent, read-only) — блокеров не нашёл; отдельно перепроверены все 6 контрольных точек (текст выделения в промпт не течёт — grep по
selection.(text|before|after)в промпте пуст; fail-closed для чужой/удалённой страницы; свежесть снапшота на каждый ход). Два nit (клиентский dedup blockIds без раннего cap; клиент не капит длину blockId — сервер режет) — defence-in-depth уже покрыт, правок не требуют.Checklist
Передаю на ревью. Реализация точно по 6 принятым решениям issue (подробности и результаты прогонов — в теле PR). Ключевое для проверки: текст выделения НЕ попадает в системный промпт (только фиксированный однострочный флаг), выделение fail-closed умирает вместе со страницей при недоступном/чужом/удалённом resolve, снапшот свежий на каждый ход через live-ref. Прогоны: сервер 164/164, клиент 35/35, tsc чисто по файлам #388. Внутренний цикл: 1 проход, блокеров нет.
🤖 Авто-ревью — feat(ai-chat): getCurrentPage отдаёт выделение пользователя (#388) → PASS ✅
Аккуратная, безопасно спроектированная фича. Объективный гейт зелёный: server
tsc0, server jest 164 passed (4 сьюта), client vitest 35 passed (2 файла). Прогонял в чистом клон-руме на реальном стеке (workspace-пакеты собраны перед tsc).✅ Проверено независимо (9 аспектов, security — построчно против кода)
resolveOpenPageContext(ai-chat.service.ts:376-422): любой не-успех возвращаетnullдля ВСЕГО контекста ДО того, как выделение хоть где-то трогается — нет id (!candidatePageId), страница не найдена / чужой воркспейс (!page || page.workspaceId !== workspace.id),validateCanViewбросил (пойман →null, fail-closed и на не-Forbidden).sanitizeSelectionзовётся только в финальном success-return. Кросс-воркспейсной утечки выделения нет.null.ai-chat.prompt.ts:322-324) идёт только фиксированный флаг по truthy-проверке; тип сужен доobject | null; ниtext/before/after/blockIdsне интерполируются. Текст выделения доходит до модели ТОЛЬКО через tool-result (data-канал, SAFETY_FRAMEWORK трактует как untrusted). Тестai-chat.prompt.spec.tsпроверяет отсутствие секретного текста в промпте.sanitizeSelection— единственный гейт сырого значения (caps text 4000 / context 200 / blockIds 20×64, мусор → null, oversize-id ДРОПается, не режется). Нет XSS-стока: клиент берётdoc.textBetween(плейн-текст), сервер отдаёт JSON,dangerouslySetInnerHTMLне добавлен.{page}→{page,selection}— чистый суперсет (ни один типизированный консьюмер не ждёт старую форму; модель — единственный консьюмер;tool-tiersобновлён, тула остаётсяcore). Контракт «hint, не ground-truth»: сервер нигде не доверяет выделению как edit-таргету; описание тулы велит локализовать через searchInPage/getNode.getEditorSelectionRef.currentпереустанавливается каждый рендер, читается вprepareSendMessagesRequest) — как существующийopenPageRef, без stale-closure; каждый ход шлёт свежее выделение. Edge-кейсы редактора обработаны (пустой каре́т/не-текст → null, мульти-блок, cap+truncate, clamp before/after; guardisDestroyed). Выделение вложено вopenPage→ умирает вместе со страницей.getCurrentPage.execute().⛔ DROP — калибровочный лог (кодеру НЕ делать)
[speculative]suggestion/low[stability]getEditorSelectionRef.current?.() ?? nullвprepareSendMessagesRequestбез try/catch. Геттер верифицированно не бросает при известных путях (guardisDestroyed, валидныйEditorState, позиции в границах), так что это защита от невозможного сегодня броска. Опциональное упрочнение (обернуть в try/catch → null, чтобы даже гипотетический бросок не рвал отправку) — но по порогу это ниже DO; ревьюер тоже «would not block».[below-threshold]suggestion/low[test-coverage] нет теста на ровно 4000 символов текста (truncated должен остаться unset приlength === cap); покрыты 5000 и короткий.[below-threshold]suggestion/low[test-coverage] путь «выделение только по не-текстовым нодам → null» (get-editor-selection.ts:44) и строка :67 не покрыты.[below-threshold]suggestion/low[test-coverage] колбэкgetEditorSelectionвai-chat-window.tsx(edit-vs-readonly,isDestroyed) без юнит-теста (glue-код).[below-threshold]suggestion/low[test-coverage] гарантия «свежий снапшот на каждую отправку» структурная, но не проверена на ДВУХ последовательных отправках (одиночная — проверена).🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge. Гейт (чистый клон-рум): server tsc 0 · jest 164 · client vitest 35. База develop
ebf132d5.