feat(ai-chat): getCurrentPage отдаёт текущее выделение пользователя в редакторе («вот тут исправь») #388

Closed
opened 2026-07-06 16:06:52 +03:00 by agent_vscode · 0 comments
Collaborator

Зачем

Пользователь выделяет фрагмент текста на странице и говорит агенту «вот тут исправь» / «переформулируй это». Сегодня агент знает только страницу (getCurrentPage{ page: { id, title } }), но не место: ему приходится переспрашивать или угадывать.

Цель: снимать выделение редактора в момент отправки сообщения, возить его тем же путём, что и openPage, и отдавать модели через существующую core-тулу getCurrentPage — что означают «this» / «here» / «вот тут».

Принятые решения (не пересматривать)

  1. Снапшот при отправке, не live-запрос. SSE односторонний, сервер не может спросить браузер в момент вызова тулы. Выделение читается в prepareSendMessagesRequest (тот же live-ref-паттерн, что openPageRef). Каждый ход шлёт свежий снапшот → multi-turn работает сам.
  2. Интеграция в getCurrentPage, не отдельная тула. Она core (всегда активна, tool-tiers.ts), нулевой рост каталога; «страница + выделение» — один вопрос «где пользователь».
  3. Выделение вложено ВНУТРЬ openPage. Если страница не резолвится (resolveOpenPageContext → null: чужая / недоступная / удалённая), выделение умирает вместе с ней автоматически. Отдельного pageId у selection нет.
  4. Текст выделения — только в tool result, в системный промпт — лишь однострочный флаг. Текст — untrusted-контент коллаборативной страницы (та же логика, что экранирование title #274 F1). В tool result SAFETY_FRAMEWORK уже трактует содержимое как данные.
  5. Выделение — hint, не ground truth (урок #159). Сервер НЕ верифицирует, что присланный текст реально есть на странице (дорого и хрупко: markdown vs plain text). Митигация — описание тулы велит агенту локализовать фрагмент (searchInPage/getNode) перед правкой; правки всё равно идут через CASL-защищённые тулы.
  6. PM-позиции (from/to) НЕ шлём. Серверные тулы ими не оперируют, протухают мгновенно. Мостик к тулам — blockIds (attrs.id блоков, unique-id extension) + before/after контекст для дизамбигуации editPageText find/replace.
  7. Чтение выделения после blur валидно. ProseMirror хранит state.selection после потери фокуса; прецедент в кодовой базе — comment-dialog.tsx:54-55 (кнопка комментария читает выделение после клика вне редактора).

⚠️ Правила для исполнителя

  1. Все решения приняты. Невозможно сделать как написано / противоречие — СТОП и вопрос комментарием в ишью.
  2. Комментарии в коде — только на английском. Точечные правки, не переписывать файлы.
  3. Не трогать: public-share-chat (свой openedPage-пайплайн — за скоупом), shared-реестр @docmost/mcp (getCurrentPage — inline-тула), схему БД, транспорт useChat.
  4. Не добавлять зависимостей.

Wire-формат (клиент → сервер, body POST /api/ai-chat/stream)

openPage: {
  id: string,
  title: string,
  selection?: {
    text: string,        // plain text of the selection (textBetween, '\n' between blocks)
    truncated?: boolean, // a cap was hit (client OR server side)
    blockIds?: string[], // attrs.id of blocks covering the selection (bridge to getNode/patchNode)
    before?: string,     // ~160 chars of plain text right before the selection
    after?: string,      // ~160 chars right after — disambiguates duplicates for editPageText
  } | null
}

Капы: клиент text ≤ 2000, сервер re-cap text ≤ 4000; before/after ≤ 200 (сервер); blockIds ≤ 20 элементов × ≤ 64 символа. Пустой после трима textselection: null.

Задачи

1. Клиент

1.1. apps/client/src/features/editor/utils/get-editor-selection.ts (новый)

export interface EditorSelectionContext {
  text: string;
  truncated?: boolean;
  blockIds?: string[];
  before?: string;
  after?: string;
}

// Pure: takes an EditorState so it is unit-testable with a headless editor.
export function getEditorSelectionContext(state: EditorState): EditorSelectionContext | null;

Семантика:

  • state.selection.emptynull (дефолтная каретка-в-начале никогда не даёт ложного срабатывания);
  • text = doc.textBetween(from, to, '\n'), кап 2000 + truncated: true;
  • blockIds: doc.nodesBetween(from, to, (node) => { if (node.isBlock && node.attrs?.id) ids.push(node.attrs.id) }), дедуп, кап 20;
  • before = doc.textBetween(max(0, from - 160), from, '\n'), after = doc.textBetween(to, min(doc.content.size, to + 160), '\n');
  • пустой после трима textnull.

1.2. components/ai-chat-window.tsx

Читать оба атома (useAtomValue): pageEditorAtom, readOnlyEditorAtom (@/features/editor/atoms/editor-atoms.ts). Собрать снапшоттер и передать в <ChatThread> новым пропом:

// Snapshot the user's current editor selection at send time. Edit-mode editor
// wins; the read-only editor is the fallback (read mode). Null when neither
// holds a non-empty selection.
const getEditorSelection = useCallback((): EditorSelectionContext | null => {
  for (const editor of [pageEditor, readOnlyEditor]) {
    if (!editor || editor.isDestroyed) continue;
    const sel = getEditorSelectionContext(editor.state);
    if (sel) return sel;
  }
  return null;
}, [pageEditor, readOnlyEditor]);

1.3. components/chat-thread.tsx

  • Новый проп getEditorSelection?: () => EditorSelectionContext | null; хранить в ref (getEditorSelectionRef) по образцу openPageRef — транспортный useMemo([]) его замыкает, идентичность пропа не важна.
  • В prepareSendMessagesRequest (~:298-309) заменить сборку openPage:
// Attach the live editor selection to the open-page context at send time —
// "this"/"here" in the user's message means THIS selection. Nested inside
// openPage so it dies with the page when the server rejects the page id.
openPage: openPageRef.current
  ? { ...openPageRef.current, selection: getEditorSelectionRef.current?.() ?? null }
  : null,
  • OpenPageContext (chat-thread.tsx:46) НЕ менять — selection приклеивается только на проводе.

2. Сервер

2.1. ai-chat.service.ts — тип body

AiChatStreamBody.openPage (~:191) расширить: selection?: unknown + комментарий, что поле клиентское/недоверенное и санируется sanitizeSelection (body — loose shape, парсится с req.body без DTO).

2.2. tools/current-page.util.ts — санация + новый контракт результата

export interface SelectionContext {
  text: string;
  truncated?: boolean;
  blockIds?: string[];
  before?: string;
  after?: string;
}

// Sanitize the client-reported selection: type-check every field, cap sizes
// (text 4000, before/after 200, blockIds 20 x 64 chars), drop garbage to null.
// The selection is a CLIENT-side snapshot — never verified against the page
// content (#159 lesson: treat as a hint, not ground truth).
export function sanitizeSelection(raw: unknown): SelectionContext | null;

export interface CurrentPageResult {
  page: { id: string; title: string } | null;
  selection: SelectionContext | null; // null when nothing is selected or no page
}

resolveCurrentPageResult(openedPage) — селекшн берётся с резолвнутого контекста (см. 2.3); page: null ⇒ всегда selection: null.

2.3. ai-chat.service.tsresolveOpenPageContext (~:362)

  • Параметр openPage — расширенный тип с selection?: unknown.
  • Возврат: { id, title, updatedAt, selection: SelectionContext | null }selection: sanitizeSelection(openPage.selection) приклеивается ТОЛЬКО на успешном резолве страницы (fail-closed ветки возвращают null целиком, как сейчас).
  • Даунстрим (detectPageChange, snapshotOpenPage) лишнее поле игнорирует — по прецеденту updatedAt («The system-prompt / tool consumers ignore the extra field»).

2.4. tools/ai-chat-tools.service.tsforUser + getCurrentPage (~:157, :309-317)

  • Тип параметра openedPage расширить полем selection?: SelectionContext | null.
  • Описание тулы дополнить (полный новый текст):

'Return the page the user is currently viewing — i.e. what "this page", "the current page", or "here" refers to — plus the text the user currently has SELECTED on that page (what "this", "here", "the selected fragment" refers to), or selection: null when nothing is selected. The selection is a client-side snapshot taken when the user sent the message and includes the ids of the blocks it covers plus surrounding context; it is NOT verified server-side — locate it in the page (searchInPage / getNode) before editing. Returns page: null if the user is not currently on a page. Call this first whenever the user refers to the current page or a selected fragment without giving an explicit id.'

  • execute не меняется (resolveCurrentPageResult(openedPage) — селекшн уже внутри контекста).

2.5. ai-chat.prompt.ts — флаг в контекст (~:301-312)

BuildSystemPromptInput.openedPage (~:160) расширить selection?: object | null. После существующей строки про страницу, при наличии selection добавить ФИКСИРОВАННУЮ строку (без текста выделения — untrusted):

if (openedPage?.selection) {
  context += `\nThe user currently has text SELECTED on this page — call getCurrentPage to see the selection. When they say "this", "here", "the selected text" or similar, they mean that selection.`;
}

2.6. tools/tool-tiers.ts (~:101)

catalogLine: 'getCurrentPage — the page the user is currently viewing and their current text selection on it.'

3. Тесты

Клиент:

  • get-editor-selection.test.ts: headless-редактор (по образцу тестов packages/editor-ext) — пустое выделение → null; однопараграфное → text без blockSeparator-артефактов; мультиблочное → '\n' между блоками + blockIds всех накрытых блоков; кап текста → truncated; before/after у границ дока не выходят за 0/size; дефолтная каретка нового редактора → null.
  • chat-thread.test.tsx (мок-харнесс уже ловит транспорт): body содержит openPage.selection из геттера в момент send; геттер вернул null → selection: null; openPage === null (не-страничный роут) → selection не шлётся вовсе.

Сервер:

  • current-page.util.spec.ts: sanitizeSelection — валидный payload проходит; не-объект/пустой text/мусорные типы полей → null или отбрасывание поля; серверные капы (text 4000 → truncated, blockIds > 20 → срез, элемент > 64 симв. → отбрасывание); resolveCurrentPageResult{ page, selection } контракт, page: nullselection: null.
  • ai-chat.service.spec.ts: resolveOpenPageContext приклеивает санированный selection на успешном резолве; foreign/inaccessible страница → null целиком (selection не выживает).
  • ai-chat-tools.service.spec.ts: getCurrentPage возвращает selection из переданного контекста; без selection — selection: null.
  • ai-chat.prompt.spec.ts: строка-подсказка присутствует ⇔ selection есть; ТЕКСТ выделения в промпт не попадает никогда.

Ожидаемое поведение

Случай Ожидание
Выделен фрагмент, «вот тут исправь» getCurrentPage{ page, selection: { text, blockIds, before, after } }; агент локализует через searchInPage/getNode и правит.
Ничего не выделено / редактор не фокусировался selection: null, промпт без строки-подсказки — байт-в-байт сегодняшнее поведение.
Select-all огромной страницы Двойной кап (2000/4000) + truncated: true; blockIds позволяют дочитать через getNode.
Выделение + невалидный/чужой openPage.id resolveOpenPageContext → null, selection умирает вместе со страницей (решение 3).
Два таба Выделение читается в табе, который шлёт сообщение, из его же editor state — рассинхрон невозможен по построению.
Read-режим страницы Работает через readOnlyEditorAtom (fallback в 1.2).
Спуф/протухший selection Санация капами; агент по описанию тулы верифицирует фрагмент на странице; правки — через CASL-тулы. Эскалации нет.
Гонка навигации (атом ещё держит editor прошлой страницы) Микроокно; худший исход — selection с прошлой страницы при валидном новом id: агент не найдёт текст и переспросит. Принято как ограничение v1.

DoD

  • tsc + build обоих приложений; существующие тесты зелёные (правки ожидаются только в перечисленных спеках).
  • Без выделения — поведение байт-в-байт (единственное контрактное изменение — добавленный ключ selection в результате getCurrentPage; клиентский tool-parts.tsx читает o.id/o.pageId и на вложенный page не реагирует уже сегодня — additive-ключ безопасен).
  • grep: текст выделения не попадает в buildSystemPrompt ни одним путём.

Не входит в объём

  • public-share-chat (расширяется симметрично отдельной задачей при необходимости);
  • caret-режим («на каком абзаце стою» без выделения) — v2: требует отсечения дефолтной каретки-в-начале по факту фокуса;
  • live-запрос выделения в момент вызова тулы (нужен обратный канал сервер→клиент);
  • серверная верификация текста выделения против контента страницы.

Связанное

  • #159 — урок про attacker-controllable клиентский page-контекст (title spoofing) — решения 3 и 5.
  • #274 — untrusted-контент страницы в промпте (F1) — решение 4.
  • #332 — tool tiers: getCurrentPage core, интеграция без роста каталога.
# Зачем Пользователь выделяет фрагмент текста на странице и говорит агенту «вот тут исправь» / «переформулируй это». Сегодня агент знает только страницу (`getCurrentPage` → `{ page: { id, title } }`), но не место: ему приходится переспрашивать или угадывать. **Цель:** снимать выделение редактора в момент отправки сообщения, возить его тем же путём, что и `openPage`, и отдавать модели через существующую core-тулу `getCurrentPage` — что означают «this» / «here» / «вот тут». # Принятые решения (не пересматривать) 1. **Снапшот при отправке, не live-запрос.** SSE односторонний, сервер не может спросить браузер в момент вызова тулы. Выделение читается в `prepareSendMessagesRequest` (тот же live-ref-паттерн, что `openPageRef`). Каждый ход шлёт свежий снапшот → multi-turn работает сам. 2. **Интеграция в `getCurrentPage`, не отдельная тула.** Она core (всегда активна, `tool-tiers.ts`), нулевой рост каталога; «страница + выделение» — один вопрос «где пользователь». 3. **Выделение вложено ВНУТРЬ `openPage`.** Если страница не резолвится (`resolveOpenPageContext` → null: чужая / недоступная / удалённая), выделение умирает вместе с ней автоматически. Отдельного pageId у selection нет. 4. **Текст выделения — только в tool result, в системный промпт — лишь однострочный флаг.** Текст — untrusted-контент коллаборативной страницы (та же логика, что экранирование title #274 F1). В tool result SAFETY_FRAMEWORK уже трактует содержимое как данные. 5. **Выделение — hint, не ground truth (урок #159).** Сервер НЕ верифицирует, что присланный текст реально есть на странице (дорого и хрупко: markdown vs plain text). Митигация — описание тулы велит агенту локализовать фрагмент (`searchInPage`/`getNode`) перед правкой; правки всё равно идут через CASL-защищённые тулы. 6. **PM-позиции (`from`/`to`) НЕ шлём.** Серверные тулы ими не оперируют, протухают мгновенно. Мостик к тулам — `blockIds` (attrs.id блоков, unique-id extension) + `before`/`after` контекст для дизамбигуации `editPageText` find/replace. 7. **Чтение выделения после blur валидно.** ProseMirror хранит `state.selection` после потери фокуса; прецедент в кодовой базе — `comment-dialog.tsx:54-55` (кнопка комментария читает выделение после клика вне редактора). # ⚠️ Правила для исполнителя 1. Все решения приняты. Невозможно сделать как написано / противоречие — СТОП и вопрос комментарием в ишью. 2. Комментарии в коде — только на английском. Точечные правки, не переписывать файлы. 3. **Не трогать**: public-share-chat (свой `openedPage`-пайплайн — за скоупом), shared-реестр `@docmost/mcp` (getCurrentPage — inline-тула), схему БД, транспорт `useChat`. 4. Не добавлять зависимостей. # Wire-формат (клиент → сервер, body `POST /api/ai-chat/stream`) ```ts openPage: { id: string, title: string, selection?: { text: string, // plain text of the selection (textBetween, '\n' between blocks) truncated?: boolean, // a cap was hit (client OR server side) blockIds?: string[], // attrs.id of blocks covering the selection (bridge to getNode/patchNode) before?: string, // ~160 chars of plain text right before the selection after?: string, // ~160 chars right after — disambiguates duplicates for editPageText } | null } ``` Капы: клиент `text` ≤ 2000, сервер re-cap `text` ≤ 4000; `before`/`after` ≤ 200 (сервер); `blockIds` ≤ 20 элементов × ≤ 64 символа. Пустой после трима `text` → `selection: null`. # Задачи ## 1. Клиент ### 1.1. `apps/client/src/features/editor/utils/get-editor-selection.ts` (новый) ```ts export interface EditorSelectionContext { text: string; truncated?: boolean; blockIds?: string[]; before?: string; after?: string; } // Pure: takes an EditorState so it is unit-testable with a headless editor. export function getEditorSelectionContext(state: EditorState): EditorSelectionContext | null; ``` Семантика: - `state.selection.empty` → `null` (дефолтная каретка-в-начале никогда не даёт ложного срабатывания); - `text = doc.textBetween(from, to, '\n')`, кап 2000 + `truncated: true`; - `blockIds`: `doc.nodesBetween(from, to, (node) => { if (node.isBlock && node.attrs?.id) ids.push(node.attrs.id) })`, дедуп, кап 20; - `before = doc.textBetween(max(0, from - 160), from, '\n')`, `after = doc.textBetween(to, min(doc.content.size, to + 160), '\n')`; - пустой после трима `text` → `null`. ### 1.2. `components/ai-chat-window.tsx` Читать оба атома (`useAtomValue`): `pageEditorAtom`, `readOnlyEditorAtom` (`@/features/editor/atoms/editor-atoms.ts`). Собрать снапшоттер и передать в `<ChatThread>` новым пропом: ```ts // Snapshot the user's current editor selection at send time. Edit-mode editor // wins; the read-only editor is the fallback (read mode). Null when neither // holds a non-empty selection. const getEditorSelection = useCallback((): EditorSelectionContext | null => { for (const editor of [pageEditor, readOnlyEditor]) { if (!editor || editor.isDestroyed) continue; const sel = getEditorSelectionContext(editor.state); if (sel) return sel; } return null; }, [pageEditor, readOnlyEditor]); ``` ### 1.3. `components/chat-thread.tsx` - Новый проп `getEditorSelection?: () => EditorSelectionContext | null`; хранить в ref (`getEditorSelectionRef`) по образцу `openPageRef` — транспортный `useMemo([])` его замыкает, идентичность пропа не важна. - В `prepareSendMessagesRequest` (~:298-309) заменить сборку `openPage`: ```ts // Attach the live editor selection to the open-page context at send time — // "this"/"here" in the user's message means THIS selection. Nested inside // openPage so it dies with the page when the server rejects the page id. openPage: openPageRef.current ? { ...openPageRef.current, selection: getEditorSelectionRef.current?.() ?? null } : null, ``` - `OpenPageContext` (chat-thread.tsx:46) НЕ менять — selection приклеивается только на проводе. ## 2. Сервер ### 2.1. `ai-chat.service.ts` — тип body `AiChatStreamBody.openPage` (~:191) расширить: `selection?: unknown` + комментарий, что поле клиентское/недоверенное и санируется `sanitizeSelection` (body — loose shape, парсится с `req.body` без DTO). ### 2.2. `tools/current-page.util.ts` — санация + новый контракт результата ```ts export interface SelectionContext { text: string; truncated?: boolean; blockIds?: string[]; before?: string; after?: string; } // Sanitize the client-reported selection: type-check every field, cap sizes // (text 4000, before/after 200, blockIds 20 x 64 chars), drop garbage to null. // The selection is a CLIENT-side snapshot — never verified against the page // content (#159 lesson: treat as a hint, not ground truth). export function sanitizeSelection(raw: unknown): SelectionContext | null; export interface CurrentPageResult { page: { id: string; title: string } | null; selection: SelectionContext | null; // null when nothing is selected or no page } ``` `resolveCurrentPageResult(openedPage)` — селекшн берётся с резолвнутого контекста (см. 2.3); `page: null` ⇒ всегда `selection: null`. ### 2.3. `ai-chat.service.ts` — `resolveOpenPageContext` (~:362) - Параметр `openPage` — расширенный тип с `selection?: unknown`. - Возврат: `{ id, title, updatedAt, selection: SelectionContext | null }` — `selection: sanitizeSelection(openPage.selection)` приклеивается ТОЛЬКО на успешном резолве страницы (fail-closed ветки возвращают null целиком, как сейчас). - Даунстрим (`detectPageChange`, `snapshotOpenPage`) лишнее поле игнорирует — по прецеденту `updatedAt` («The system-prompt / tool consumers ignore the extra field»). ### 2.4. `tools/ai-chat-tools.service.ts` — `forUser` + `getCurrentPage` (~:157, :309-317) - Тип параметра `openedPage` расширить полем `selection?: SelectionContext | null`. - Описание тулы дополнить (полный новый текст): > 'Return the page the user is currently viewing — i.e. what "this page", "the current page", or "here" refers to — plus the text the user currently has SELECTED on that page (what "this", "here", "the selected fragment" refers to), or selection: null when nothing is selected. The selection is a client-side snapshot taken when the user sent the message and includes the ids of the blocks it covers plus surrounding context; it is NOT verified server-side — locate it in the page (searchInPage / getNode) before editing. Returns page: null if the user is not currently on a page. Call this first whenever the user refers to the current page or a selected fragment without giving an explicit id.' - `execute` не меняется (`resolveCurrentPageResult(openedPage)` — селекшн уже внутри контекста). ### 2.5. `ai-chat.prompt.ts` — флаг в контекст (~:301-312) `BuildSystemPromptInput.openedPage` (~:160) расширить `selection?: object | null`. После существующей строки про страницу, при наличии selection добавить ФИКСИРОВАННУЮ строку (без текста выделения — untrusted): ```ts if (openedPage?.selection) { context += `\nThe user currently has text SELECTED on this page — call getCurrentPage to see the selection. When they say "this", "here", "the selected text" or similar, they mean that selection.`; } ``` ### 2.6. `tools/tool-tiers.ts` (~:101) `catalogLine: 'getCurrentPage — the page the user is currently viewing and their current text selection on it.'` ## 3. Тесты **Клиент:** - `get-editor-selection.test.ts`: headless-редактор (по образцу тестов `packages/editor-ext`) — пустое выделение → null; однопараграфное → text без blockSeparator-артефактов; мультиблочное → `'\n'` между блоками + blockIds всех накрытых блоков; кап текста → truncated; before/after у границ дока не выходят за 0/size; дефолтная каретка нового редактора → null. - `chat-thread.test.tsx` (мок-харнесс уже ловит транспорт): body содержит `openPage.selection` из геттера в момент send; геттер вернул null → `selection: null`; `openPage === null` (не-страничный роут) → selection не шлётся вовсе. **Сервер:** - `current-page.util.spec.ts`: `sanitizeSelection` — валидный payload проходит; не-объект/пустой text/мусорные типы полей → null или отбрасывание поля; серверные капы (text 4000 → truncated, blockIds > 20 → срез, элемент > 64 симв. → отбрасывание); `resolveCurrentPageResult` — `{ page, selection }` контракт, `page: null` ⇒ `selection: null`. - `ai-chat.service.spec.ts`: `resolveOpenPageContext` приклеивает санированный selection на успешном резолве; foreign/inaccessible страница → null целиком (selection не выживает). - `ai-chat-tools.service.spec.ts`: `getCurrentPage` возвращает selection из переданного контекста; без selection — `selection: null`. - `ai-chat.prompt.spec.ts`: строка-подсказка присутствует ⇔ selection есть; ТЕКСТ выделения в промпт не попадает никогда. # Ожидаемое поведение | Случай | Ожидание | |---|---| | Выделен фрагмент, «вот тут исправь» | `getCurrentPage` → `{ page, selection: { text, blockIds, before, after } }`; агент локализует через searchInPage/getNode и правит. | | Ничего не выделено / редактор не фокусировался | `selection: null`, промпт без строки-подсказки — байт-в-байт сегодняшнее поведение. | | Select-all огромной страницы | Двойной кап (2000/4000) + `truncated: true`; blockIds позволяют дочитать через getNode. | | Выделение + невалидный/чужой `openPage.id` | `resolveOpenPageContext` → null, selection умирает вместе со страницей (решение 3). | | Два таба | Выделение читается в табе, который шлёт сообщение, из его же editor state — рассинхрон невозможен по построению. | | Read-режим страницы | Работает через `readOnlyEditorAtom` (fallback в 1.2). | | Спуф/протухший selection | Санация капами; агент по описанию тулы верифицирует фрагмент на странице; правки — через CASL-тулы. Эскалации нет. | | Гонка навигации (атом ещё держит editor прошлой страницы) | Микроокно; худший исход — selection с прошлой страницы при валидном новом id: агент не найдёт текст и переспросит. Принято как ограничение v1. | # DoD - tsc + build обоих приложений; существующие тесты зелёные (правки ожидаются только в перечисленных спеках). - Без выделения — поведение байт-в-байт (единственное контрактное изменение — добавленный ключ `selection` в результате `getCurrentPage`; клиентский `tool-parts.tsx` читает `o.id`/`o.pageId` и на вложенный `page` не реагирует уже сегодня — additive-ключ безопасен). - grep: текст выделения не попадает в `buildSystemPrompt` ни одним путём. # Не входит в объём - public-share-chat (расширяется симметрично отдельной задачей при необходимости); - caret-режим («на каком абзаце стою» без выделения) — v2: требует отсечения дефолтной каретки-в-начале по факту фокуса; - live-запрос выделения в момент вызова тулы (нужен обратный канал сервер→клиент); - серверная верификация текста выделения против контента страницы. # Связанное - #159 — урок про attacker-controllable клиентский page-контекст (title spoofing) — решения 3 и 5. - #274 — untrusted-контент страницы в промпте (F1) — решение 4. - #332 — tool tiers: getCurrentPage core, интеграция без роста каталога.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#388