| 1 | # ADR 0128: Intercom — якоря вложений (code references) и канонический attach |
| 2 | |
| 3 | **Статус:** Accepted · Implemented |
| 4 | **Дата:** 2026-05-19 |
| 5 | |
| 6 | ## Связанные ADR |
| 7 | |
| 8 | | ADR | Роль | |
| 9 | |-----|------| |
| 10 | | [0080](0080-intercom-naming-and-multi-party-channel-model.md) | Intercom как канал; deep links; multi-party | |
| 11 | | [0119](0119-chat-slash-commands-intercom-surface.md) | Slash в composer; autocomplete; local execution | |
| 12 | | [0124](0124-slash-parametric-editor-line-commands.md) | `/editor line …` — **действие** в редакторе; binders строк 1-based | |
| 13 | | [0125](0125-slash-workspace-file-commands-and-dynamic-completion.md) | `/file open` — **навигация**; dynamic completion путей | |
| 14 | | [0045](0045-agent-chat-persistence-event-log-and-projections.md) | Event log; вложения в теле сообщения | |
| 15 | | [0030](0030-command-ids-hotkeys-and-ui-registry-layers.md) | `command_id`; паритет MCP | |
| 16 | | [0058](0058-agent-roslyn-mcp-coupling-settings-toml.md) | Resolve member / semantic | |
| 17 | | [0111](0111-editor-linenumber-linerange-value-objects.md) | `LineRange` в домене | |
| 18 | | [0053](0053-semantic-map-control-flow-pfd.md) | Structural picker внутри метода (v2+) | |
| 19 | | [0120](0120-primary-work-surface-intercom-or-editor.md) | Composer в Forward | |
| 20 | | [0123](0123-intercom-full-skia-surface-evolution.md) | Skia-лента | |
| 21 | | [0129](0129-intercom-message-body-markdown-and-fenced-code.md) | **Fenced code** и markdown в `content` — **не** этот ADR | |
| 22 | | [0130](0130-editor-agent-range-reveal-without-selection.md) | MCP **reveal** диапазона без selection (агент); общий presentation mode с §8 | |
| 23 | | [0131](0131-editor-slash-select-code-by-bracket-reference.md) | `/editor select code [M:…]` — bracket → select в редакторе, не attach | |
| 24 | | [0137](0137-intercom-message-code-correspondence.md) | `anchor:<id>` в find/relate; message ordinals | |
| 25 | | [0138](0138-cockpit-command-line-and-parametric-ranges.md) | CCL — единственный REPL Forward; anchor list/peek без hit-test | |
| 26 | |
| 27 | ### Вне ADR (playbook) |
| 28 | |
| 29 | | Документ | Роль | |
| 30 | |----------|------| |
| 31 | | [intercom-ux-reference-slack-mattermost-v1.md](../design/intercom-ux-reference-slack-mattermost-v1.md) | Slack/MM границы, flat feed, макеты — **продуктовый чертёж** | |
| 32 | | [intercom-design-hub-v1.md](../design/intercom-design-hub-v1.md) | Домены D1–D9 для дизайнера | |
| 33 | |
| 34 | ## Резюме |
| 35 | |
| 36 | Зафиксировать **единую модель «упомянуть кусок кода в репозитории»** в Intercom: не путь/строки в голове пользователя, а **смысловой якорь** (выделение, member, синтаксический scope, файл), с **производным** снимком `file` + `LineRange` @ send и **re-resolve** у получателя. |
| 37 | |
| 38 | 1. **Attach** (`/attach …`, `[…]`, chips) — payload к **черновику/сообщению**; ортогонален `/file open` и `/editor line`. |
| 39 | 2. **Канон в wire:** `AttachmentAnchor` (shape, optional `memberKey`, optional `syntaxScope`, `excerpt`, resolved `file` + lines @ send, offsets в prose). |
| 40 | 3. **Человек:** приоритет **H0** selection, **H0b** scope @ caret, **H1** `[M:…]`; **M0** path/lines — fallback для агента. |
| 41 | 4. **Клик из ленты:** `intercom.reveal_attachment` — open + scroll + **рамка** (не selection); Shift → select. |
| 42 | 5. **Строки @ send** — не контракт между участниками; устойчивее **excerpt** и **member** / **syntaxScope**. |
| 43 | |
| 44 | **Не входит:** vision/policy для image в prompt агента; полный structural picker (v2); `@file` inline (запасной); реализация composer chips (отдельные задачи); **fenced code / markdown в теле** — [0129](0129-intercom-message-body-markdown-and-fenced-code.md). |
| 45 | |
| 46 | **Два вида «кода» в чате:** (1) **якорь на workspace** — этот ADR; (2) **цитата в `content`** (`` ``` ``) — [0129](0129-intercom-message-body-markdown-and-fenced-code.md). |
| 47 | |
| 48 | --- |
| 49 | |
| 50 | ## Контекст |
| 51 | |
| 52 | Оператор хочет в одной реплике сказать: *«этот метод здесь, а регрессия — там»*, в том числе **в середине предложения**. Кажется простым, но смешивает: |
| 53 | |
| 54 | | Поверхность | Пример | Эффект | |
| 55 | |-------------|--------|--------| |
| 56 | | Attach | `/attach selection`, `[M:Foo]` | Payload агенту + deep link | |
| 57 | | Editor action | `/editor line select 5 10` | Меняет selection в буфере | |
| 58 | | Open file | `/file open Foo.cs` | Только вкладка | |
| 59 | | Agent MCP | `go_to_position` | Select в редакторе (правка) | |
| 60 | | Fenced в ответе | `` ```csharp … ``` `` | Только текст в ленте — [0129](0129-intercom-message-body-markdown-and-fenced-code.md) | |
| 61 | |
| 62 | Без ADR неизбежны: `@cc:`-стиль, пузыри, «строка 50» как единственная правда, случайное удаление после клика из чата, рассинхрон строк у отправителя и получателя. |
| 63 | |
| 64 | **Процесс:** домен проговаривали с агентом как с собеседником до кода (ветвления attach/клик/re-resolve) → этот ADR и [intercom-design-hub](../design/intercom-design-hub-v1.md); капитан и ревью — у человека — [philosophy §8](../design/cascadeide-philosophy-v1.md#8-агент-как-партнёр-для-проектирования-до-кода). |
| 65 | |
| 66 | --- |
| 67 | |
| 68 | ## Проблема |
| 69 | |
| 70 | 1. **Когнитивная нагрузка:** требовать path + line numbers в prose. |
| 71 | 2. **Две правды по строкам:** ветка, локальные правки, другой монитор/контекст — L50 у отправителя ≠ L50 у получателя. |
| 72 | 3. **Гранулярность:** `[M:Foo]` на 500 строк не покрывает «второй `for`» без имени. |
| 73 | 4. **Разрыв входов:** slash, скобки, chips, MCP должны сходиться в **один** wire-тип для [0045](0045-agent-chat-persistence-event-log-and-projections.md). |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## Решение |
| 78 | |
| 79 | <a id="adr0128-p1"></a> |
| 80 | |
| 81 | ### 1. Термины |
| 82 | |
| 83 | | Термин | Значение | |
| 84 | |--------|----------| |
| 85 | | **Attachment** | Ссылка на workspace-артефакт в **теле** реплики (не отдельная «полоса вложений» только сверху) | |
| 86 | | **AttachmentAnchor** | Каноническая структура в event log | |
| 87 | | **Chip** | UI-токен в composer; после send — inline-метка в flat feed | |
| 88 | | **Resolve @ send** | Roslyn/редактор вычисляют `file`, `LineRange`, `excerpt` в момент отправки | |
| 89 | | **Reveal** | Навигация по клику из ленты (рамка, не selection по умолчанию) | |
| 90 | |
| 91 | <a id="adr0128-p2"></a> |
| 92 | |
| 93 | ### 2. `attachmentShape` (обязательное поле) |
| 94 | |
| 95 | | Shape | Когда | Строки в resolve | |
| 96 | |-------|--------|------------------| |
| 97 | | `selection` | Выделение в редакторе | да | |
| 98 | | `text-range` | Текстовый файл + диапазон / positional M0 | да | |
| 99 | | `whole-file` | Медиа, бинарники, «весь файл» | нет | |
| 100 | | `member` | Resolve по `memberKey` (весь член или уточнённый scope) | да (снимок @ send) | |
| 101 | | `syntax-scope` | Innermost `ForStatement` / `IfStatement` @ caret (H0b) | да (снимок @ send) | |
| 102 | |
| 103 | Для `.png`, `.dll` и т.д. — только `whole-file`; slash `[path] [start] [end]` для binary → **ошибка** (как I3 у [0124](0124-slash-parametric-editor-line-commands.md)). |
| 104 | |
| 105 | <a id="adr0128-p3"></a> |
| 106 | |
| 107 | ### 3. Канон `AttachmentAnchor` (wire / event log) |
| 108 | |
| 109 | Минимальный контракт для [0045](0045-agent-chat-persistence-event-log-and-projections.md) (имена полей — при реализации в JSON/schema): |
| 110 | |
| 111 | | Поле | Обязательность | Смысл | |
| 112 | |------|----------------|--------| |
| 113 | | `id` | да | Стабильный id внутри сообщения | |
| 114 | | `attachmentShape` | да | см. §2 | |
| 115 | | `displayLabel` | да | Короткая метка в ленте (`GetUserAsync`, `Foo › for (2)`) | |
| 116 | | `file` | почти всегда | Workspace-relative path после resolve | |
| 117 | | `lineStart`, `lineEnd` | если применимо | 1-based inclusive @ send — **hint**, не контракт между участниками | |
| 118 | | `memberKey` | опционально | Roslyn-qualified или согласованный stable key | |
| 119 | | `syntaxScope` | опционально | `{ kind: "for", indexInParent: 2, parentMemberKey }` — v2 | |
| 120 | | `excerpt` | рекомендуется | Текст фрагмента @ send — **устойчивый** для всех участников | |
| 121 | | `proseStart`, `proseLength` | да | Offset inline-метки в теле сообщения | |
| 122 | | `resolvedAtUtc` | да | Момент resolve @ send | |
| 123 | | `resolveOutcome` | опционально | Кэш последнего reveal у получателя: `resolved` \| `file_missing` \| … — §9.1 | |
| 124 | |
| 125 | **Принцип:** то, что видит человек в ленте — **смысл** (`displayLabel`, excerpt); path/lines — вторично (hover, агент, fallback). При `file_missing` excerpt остаётся источником правды для всех. |
| 126 | |
| 127 | <a id="adr0128-p3b"></a> |
| 128 | |
| 129 | #### 3.1 Контекст отправителя @ send (`senderWorkspaceContext`) |
| 130 | |
| 131 | **Да, ветку (и короткий commit) стоит сохранять** — как **подсказку**, не как часть re-resolve. |
| 132 | |
| 133 | | Правило | Смысл | |
| 134 | |---------|--------| |
| 135 | | **Уровень** | На **сообщение** в payload [0045](0045-agent-chat-persistence-event-log-and-projections.md) (`message_added` / `message_completed`), **не** дублировать на каждый `AttachmentAnchor` | |
| 136 | | **Обязательность** | Опционально; заполнять при send, если git repo доступен | |
| 137 | | **Использование** | UI («отправитель был на `feature/x`»), агент в prompt, кнопка «переключиться?» — **не** автоматический checkout | |
| 138 | | **Не использовать для** | Вычисления `lineStart`/`lineEnd` у получателя; единственного ключа навигации | |
| 139 | |
| 140 | Рекомендуемая форма *(имена при schema bump)*: |
| 141 | |
| 142 | ```json |
| 143 | "senderWorkspaceContext": { |
| 144 | "gitBranch": "feature/intercom-attach", |
| 145 | "gitCommitShort": "a1b2c3d", |
| 146 | "solutionPath": "relative/or/abs path to .sln at send", |
| 147 | "capturedAtUtc": "2026-05-19T12:00:00Z" |
| 148 | } |
| 149 | ``` |
| 150 | |
| 151 | | Поле | Заметки | |
| 152 | |------|---------| |
| 153 | | `gitBranch` | Имя ветки или `HEAD (detached)`; при detached — опираться на `gitCommitShort` | |
| 154 | | `gitCommitShort` | 7–12 hex; для «та же ревизия?» у получателя | |
| 155 | | `solutionPath` | Какой sln был активен @ send (multi-sln workspace) | |
| 156 | | `capturedAtUtc` | Момент снимка (может совпадать с `resolvedAtUtc` у anchors) | |
| 157 | |
| 158 | **UX при `file_missing`:** toast + «файла нет в **твоём** workspace; отправитель: `feature/x` @ `a1b2c3d`» + excerpt; опционально действие **«Checkout ветки отправителя»** (явное, v2+, не silent). |
| 159 | |
| 160 | **Риск:** ветка устарела / force-push — поле **информационное**, может не совпадать с историей; не скрывать excerpt. |
| 161 | |
| 162 | <a id="adr0128-p4"></a> |
| 163 | |
| 164 | ### 4. Ввод для человека (слои H / M) |
| 165 | |
| 166 | Приоритет **не** «сначала путь и строки»: |
| 167 | |
| 168 | | Слой | Ввод | `command_id` / механизм | |
| 169 | |------|------|-------------------------| |
| 170 | | **H0** | Выделение → attach | `attach_selection` → `/attach selection` | |
| 171 | | **H0b** | Каретка в `for` → attach scope | `attach_scope` → `/attach scope` *(фаза 3)* | |
| 172 | | **H1** | `[M:Method]`, `[Foo.cs M:Method]` | Parse bracket → resolve member | |
| 173 | | **H2** | `[diagram.png]`, `[appsettings.json]` | `whole-file` | |
| 174 | | **M0** | `[Foo.cs 50 100]`, `[F:…; L:…]` | Агент, paste; positional fallback | |
| 175 | |
| 176 | **Внутри длинного метода** без имени: H0, H0b, или H1 + prose («второй for»); v2 — `syntaxScope` / picker ([0053](0053-semantic-map-control-flow-pfd.md)). |
| 177 | |
| 178 | <a id="adr0128-p5"></a> |
| 179 | |
| 180 | ### 5. Inline `[…]` — вторая поверхность, тот же anchor |
| 181 | |
| 182 | - `[M:GetUserAsync]` — **M**; **F** (файл) из active file или autocomplete, если путь не указан. |
| 183 | - `[Foo.cs M:…]` — явный **F** + **M**. |
| 184 | - Не путать: `@` — **люди**; `[` — **артефакты**; не markdown `[](url)`. |
| 185 | - **L2** для агента: поля через **`;`** (единственный канон), тот же parse tree, что L1. |
| 186 | |
| 187 | #### 5.1 Оси полей bracket (`F` | `M` | `L` | `S`) |
| 188 | |
| 189 | Четыре **ортогональные** оси в одном `[…]` или в L2. В wire они попадают в те же поля `AttachmentAnchor` (`file`, `memberKey`, `lineStart`/`lineEnd`, `syntaxScope`). **`B:` (block)** не вводим — путается с **branch** (git); для `for`/`if`/`while` внутри члена — ось **`S:`** (syntax scope / statement). |
| 190 | |
| 191 | | Ось | Префикс | Смысл | Wire / resolve | |
| 192 | |-----|---------|--------|----------------| |
| 193 | | **File** | `F:` | Workspace-relative или абсолютный путь | `file` | |
| 194 | | **Member** | `M:` | Метод, свойство, тип (stable / Roslyn key) | `memberKey` + re-resolve | |
| 195 | | **Lines** | `L:` | Позиционный fallback (1-based inclusive) | `lineStart`, `lineEnd` @ send (hint) | |
| 196 | | **Scope** | `S:` | Синтаксический фрагмент **внутри** члена | `syntaxScope`: `{ kind, indexInParent, parentMemberKey? }` | |
| 197 | |
| 198 | **`S:` — семантика индекса:** `S:for:2` = **второй** узел kind `for` (**1-based** `indexInParent`) от **начала тела** члена `M:…` (или текущего member при H0b). Не «второй for в файле». Kind — lowercase Roslyn-имя узла: `for`, `if`, `while`, `switch`, … (как в `AttachmentSyntaxScope.Kind`). |
| 199 | |
| 200 | **Примеры L1 (человек):** |
| 201 | |
| 202 | | Выражение | Разбор | |
| 203 | |-----------|--------| |
| 204 | | `[M:Run]` | только member | |
| 205 | | `[M:Run S:for:2]` | member + второй `for` в `Run` | |
| 206 | | `[M:Run S:for(2)]` | то же; скобки `()` — только читаемость; **канон для парсера:** `S:for:2` | |
| 207 | | `[Foo.cs M:Bar S:if:1]` | явный файл + member + scope | |
| 208 | | `[S:for:2]` | только scope (нужен active file + member @ caret или H0b) | |
| 209 | |
| 210 | **Примеры L2 (агент, `;`):** |
| 211 | |
| 212 | ```text |
| 213 | [F:src/Foo.cs; M:Run; S:for:2] |
| 214 | [F:src/Foo.cs; M:Run; L:50-100] |
| 215 | [F:src/Foo.cs; M:Run; S:for:2; L:50-100] |
| 216 | ``` |
| 217 | |
| 218 | `L:` и `S:` могут сосуществовать: при re-resolve у получателя приоритет **member + syntaxScope**; `L:` — fallback @ send и при `member_not_found` (§8–9.1). |
| 219 | |
| 220 | **Связь с фазой 4:** UI picker и chip label (`displayLabel`: `Run › for (2)`) используют ту же ось **S:**, что prose `[M:Run S:for:2]`. |
| 221 | |
| 222 | **Реализация parse (0131):** `BracketCodeReferenceParser` — `F`/`M`/`L`/`S` → `AttachmentAnchor` (в т.ч. `syntaxScope` для `S:`). MCP по-прежнему предпочитает JSON `syntax_scope`, не prose. |
| 223 | |
| 224 | Парсер attach: только в **prose**-сегментах тела (после `SplitSegments` — [0129](0129-intercom-message-body-markdown-and-fenced-code.md) §5); **не** внутри fenced code. |
| 225 | |
| 226 | <a id="adr0128-p6"></a> |
| 227 | |
| 228 | ### 6. Slash namespace `/attach` |
| 229 | |
| 230 | | Slash | `command_id` | Эффект | |
| 231 | |-------|--------------|--------| |
| 232 | | `/attach selection` | `attach_selection` | Chip из active editor selection | |
| 233 | | `/attach scope` | `attach_scope` | Chip из innermost syntax @ caret | |
| 234 | | `/attach file <path>` | `attach_file` | whole-file или prompt range для text | |
| 235 | | `/attach file <path> <start> <end>` | `attach_file` | `text-range` (text-only) | |
| 236 | |
| 237 | - Autocomplete путей — **переиспользовать** [0125](0125-slash-workspace-file-commands-and-dynamic-completion.md); другой `command_id`, чем `file_open`. |
| 238 | - ~~`/attach code`~~ — **не** использовать как корень. |
| 239 | - ~~`@cc:`~~ — **не** использовать. |
| 240 | |
| 241 | Запись в `intent-catalog.toml` — по [0119](0119-chat-slash-commands-intercom-surface.md) / [0109](0109-declarative-parametric-melody-catalog-toml-and-code-binders.md) при внедрении. |
| 242 | |
| 243 | <a id="adr0128-p7"></a> |
| 244 | |
| 245 | ### 7. Ортогональные команды (не смешивать) |
| 246 | |
| 247 | | Действие | Команда | Меняет буфер? | Attach? | |
| 248 | |----------|---------|---------------|---------| |
| 249 | | Открыть файл | `/file open` [0125](0125-slash-workspace-file-commands-and-dynamic-completion.md) | нет | нет | |
| 250 | | Select/delete lines | `/editor line` [0124](0124-slash-parametric-editor-line-commands.md) | **да** | нет | |
| 251 | | Select по смыслу в редакторе | `/editor select code [M:…]` [0131](0131-editor-slash-select-code-by-bracket-reference.md) | **да** | нет | |
| 252 | | Прикрепить к реплике | `/attach …`, `[…]` | нет | **да** | |
| 253 | | Агент показывает участок | MCP `reveal_editor_range` [0130](0130-editor-agent-range-reveal-without-selection.md) | нет | нет | |
| 254 | | Агент идёт править | MCP `go_to_position` | **да** (select) | нет | |
| 255 | | Цитата кода в тексте | markdown fence | нет | нет — [0129](0129-intercom-message-body-markdown-and-fenced-code.md) | |
| 256 | |
| 257 | <a id="adr0128-p8"></a> |
| 258 | |
| 259 | ### 8. Reveal из ленты (клик по метке) |
| 260 | |
| 261 | `command_id`: **`intercom.reveal_attachment`** (или эквивалент в intent catalog). |
| 262 | |
| 263 | | Шаг | Поведение | |
| 264 | |-----|-----------| |
| 265 | | 1 | Open `file` (или preview для non-text) | |
| 266 | | 2 | Если есть `memberKey` или `syntaxScope` → **re-resolve** в solution **получателя** → `LineRange` | |
| 267 | | 3 | Иначе если есть lines @ send → best-effort + drift warning | |
| 268 | | 4a | Если `file_missing` / `member_not_found` → шаги 4–5 **не** выполнять; UI по §9.1 | |
| 269 | | 4 | Scroll into view + **transient range highlight** (рамка / gutter band), **не** `Selection` | |
| 270 | | 5 | Shift+клик или настройка → `SelectInEditor` | |
| 271 | |
| 272 | Отличие от MCP `go_to_position`: reveal = **просмотр**; go_to_position = **правка** агентом. Прямой MCP reveal без сообщения в чат — [0130](0130-editor-agent-range-reveal-without-selection.md) (`editor.reveal_range`); **`intercom.reveal_attachment`** (и slash `/editor … code`) вызывают тот же `EditorAgentRangeReveal` / `IntercomAttachmentNavigator` — см. [0130](0130-editor-agent-range-reveal-without-selection.md), [0131](0131-editor-slash-select-code-by-bracket-reference.md). |
| 273 | |
| 274 | <a id="adr0128-p9"></a> |
| 275 | |
| 276 | ### 9. Стабильность между отправителем и получателем |
| 277 | |
| 278 | - **Excerpt** и **displayLabel** (member, `Foo › for (2)`) — общий знаменатель в ленте. |
| 279 | - **lineStart/lineEnd @ send** — снимок отправителя для агента и fallback; при клике у получателя — **re-resolve** при наличии `memberKey`/`syntaxScope`. |
| 280 | - UI может показать: «при отправке L50–100» при hover, если текущий диапазон другой. |
| 281 | |
| 282 | <a id="adr0128-p9b"></a> |
| 283 | |
| 284 | ### 9.1 Re-resolve у получателя: другая ветка, нет файла, нет символа |
| 285 | |
| 286 | **Вопрос:** отправитель сослался на `Foo.cs` / `[M:Bar]`, у получателя на другой ветке файла нет (или member переименован). Что видит человек и что получает агент? |
| 287 | |
| 288 | **Принцип:** attach — это **намерение + снимок @ send**, а не гарантия, что у всех открыт **тот же** снимок workspace. Re-resolve всегда идёт в **текущем** solution/ветке **получателя**; если там нет цели — **не ломаем** ленту и не открываем пустой редактор молча. |
| 289 | |
| 290 | | Исход `resolveOutcome` *(в UI / при reveal)* | Условие | Лента (chip / hover) | Клик **reveal** | Агент в prompt | |
| 291 | |---------------------------------------------|---------|----------------------|-----------------|---------------| |
| 292 | | `resolved` | Файл есть, member/scope найден | `displayLabel` | Open + рамка по **текущим** строкам; опционально «было L50–100 @ send» | `file` + lines + **excerpt** | |
| 293 | | `file_missing` | Путь не в workspace (другая ветка, не checkout, другой sln) | Метка + иконка ⚠; **excerpt** в tooltip / expand | Toast: «`Foo.cs` нет в текущем workspace»; **не** создавать пустую вкладку; предложить **показать excerpt** (flyout) или копировать excerpt | **excerpt** + `displayLabel` + `file` (как hint); явно: *не удалось открыть в твоём дереве* | |
| 294 | | `member_not_found` | Файл есть, символа нет (rename, ветка без метода) | `displayLabel` + ⚠ stale | Open файл; рамка по **lines @ send** если валидны, иначе только scroll к файлу + warning | excerpt обязателен; memberKey как hint | |
| 295 | | `lines_drift` | Только M0 / fallback lines, диапазон пустой или сильно сдвинулся | как выше + «drift» | Open + best-effort lines + **предупреждение** | excerpt + lines @ send | |
| 296 | | `excerpt_only` | Non-text / preview-only / resolve отключён | excerpt в expand | Preview или copy excerpt | excerpt | |
| 297 | |
| 298 | **Почему excerpt обязателен @ send:** именно он **одинаков** у отправителя и получателя, когда репозитории разъехались. Строки и path — подсказки для IDE **после** успешного resolve у **этого** человека. |
| 299 | |
| 300 | **Ветка отправителя:** см. §3.1 `senderWorkspaceContext` на уровне сообщения — **рекомендуется** сохранять; не путать с re-resolve у получателя. |
| 301 | |
| 302 | **Не делаем v1:** |
| 303 | |
| 304 | - Автоматический `git checkout` ветки отправителя по клику из чата. |
| 305 | - Скрывать сообщение или chip, если файл отсутствует — смысл реплики и excerpt остаются видимыми. |
| 306 | - Притворяться, что lines @ send — общий контракт между участниками. |
| 307 | |
| 308 | **Связь с [0129](0129-intercom-message-body-markdown-and-fenced-code.md):** fenced block в ответе агента **не** зависит от файла в репо; attach chip — да. Если файла нет, получатель всё равно читает **excerpt** или fenced текст, а не «битую» ссылку. |
| 309 | |
| 310 | <a id="adr0128-p10"></a> |
| 311 | |
| 312 | ### 10. Composer и лента (UX attach) |
| 313 | |
| 314 | - Composer: **текст + chip-токены** в позиции курсора; несколько anchors в одной реплике. |
| 315 | - Лента: **flat feed** ([0123](0123-intercom-full-skia-surface-evolution.md)); inline-метки attach; без messenger-пузырей. |
| 316 | - Агенту: ordered `AttachmentAnchor[]` + prose (для LLM — excerpt обязателен для безымянных блоков). |
| 317 | - Отрисовка fenced / inline MD в prose между метками — [0129](0129-intercom-message-body-markdown-and-fenced-code.md). |
| 318 | |
| 319 | <a id="adr0128-p10b"></a> |
| 320 | |
| 321 | ### 10.1 Anchor id в CLI (CCL), без hit-test |
| 322 | |
| 323 | Wire-id: маркер `⟦a:{8 hex}⟧` (`IntercomAttachmentMarkers`) ↔ `AttachmentAnchor.Id`. Клик по chip в Skia остаётся; **второй путь** — клавиатура и CCL ([0138](0138-cockpit-command-line-and-parametric-ranges.md) [§ Инварианты внимания](#adr0138-attention-invariants)). |
| 324 | |
| 325 | | Команда *(предложение)* | Назначение | |
| 326 | |-------------------------|------------| |
| 327 | | `/intercom message anchors list` | Anchors выбранного / текущего сообщения (или черновика): `a:abcd1234`, `displayLabel`, `resolveOutcome` | |
| 328 | | `/anchor peek <id>` | Reveal по id → тот же pipeline, что `intercom.reveal_attachment` / клик chip; **без** hit-test | |
| 329 | |
| 330 | **Валидация:** как §9.1 — preview-строка в CCL (`resolved` / `file_missing` / …), excerpt остаётся fallback; Enter допускается с предупреждением на degraded. |
| 331 | |
| 332 | **ID в ленте (UX, на обсуждение):** |
| 333 | |
| 334 | | Вариант | Когда | |
| 335 | |---------|--------| |
| 336 | | Short id **справа от chip** (приглушённый `a:abcd1234`) | Быстрый ввод `/anchor peek` без `list` | |
| 337 | | Id только в **hover** / compact mode | Минимум шума в ленте | |
| 338 | | Только **`anchors list`** | Старые сообщения, узкий layout | |
| 339 | |
| 340 | **Не цель:** отдельная Shell-страница в Forward или scrollback-терминал для slash — только CCL + при необходимости MFD Terminal для OS/process. |
| 341 | |
| 342 | --- |
| 343 | |
| 344 | ## Фазы внедрения |
| 345 | |
| 346 | | Фаза | Содержание | Зависимости | CIDE | |
| 347 | |------|------------|-------------|------| |
| 348 | | **0** | Schema `AttachmentAnchor` + опционально `senderWorkspaceContext` на сообщении в [0045](0045-agent-chat-persistence-event-log-and-projections.md); projection в ленту | 0045, git | **да** — payload `attachments[]`, `sender_workspace_context`, projector, лента | |
| 349 | | **1** | `/attach selection`, `/attach file`; chips; resolve @ send; excerpt | 0125, 0111 | **да** — slash + marker в composer + `IntercomAttachmentResolveAtSend` | |
| 350 | | **2** | Bracket parse `[M:…]`, `[path]`; `/attach` в TOML; reveal рамка; **composer autocomplete** в незакрытом `[` | 0058, Roslyn, 0125 | **да** — `BracketCodeReferenceParser`; reveal/select; клик в ленте → `intercom.reveal_attachment`; `ChatBracketAutocomplete` (оси `F`/`M`/`L`/`S`, файлы [0125]) | |
| 351 | | **3** | `/attach scope`; `syntaxScope`; re-resolve @ recipient; Shift→select | 0053 опционально | **да** — `/attach scope`, `AttachmentAnchorCaretScopeResolver`, Shift+клик | |
| 352 | | **4** | Structural picker (дерево узлов @ caret); chip `displayLabel`; stale hint | 0053 | **нет** — prose autocomplete в `[` — фаза **2**; полный picker — здесь | |
| 353 | |
| 354 | --- |
| 355 | |
| 356 | ## Не цели |
| 357 | |
| 358 | - Политика image/vision в prompt агента. |
| 359 | - `@file` inline (если не понадобится после фазы 2). |
| 360 | - Замена Solution Explorer или fuzzy-поиска по репо в composer. |
| 361 | - Паритет Slack/MM server — слой B [0080 §5](0080-intercom-naming-and-multi-party-channel-model.md#adr0080-p5). |
| 362 | - Fenced code, полный markdown в ленте — [0129](0129-intercom-message-body-markdown-and-fenced-code.md). |
| 363 | |
| 364 | --- |
| 365 | |
| 366 | ## Отклонённые альтернативы |
| 367 | |
| 368 | | Альтернатива | Почему нет | |
| 369 | |--------------|------------| |
| 370 | | Только `@cc:path:lines` | Путает с `@mention`; path-first | |
| 371 | | Только positional `[file lines]` для людей | Строки не стабильны; тяжёлый ввод | |
| 372 | | Клик → always selection | Риск случайного удаления | |
| 373 | | Отдельный тип сообщения без canonical anchor | Дубли с prose; плохо для 0045 | |
| 374 | | Две грамматики `T:,M:` и `type;member:` | Двойной парсер | |
| 375 | | Fenced block = автоматический attach | Разные сущности — [0129](0129-intercom-message-body-markdown-and-fenced-code.md) | |
| 376 | |
| 377 | --- |
| 378 | |
| 379 | ## Согласованные решения (для реализации) |
| 380 | |
| 381 | | Тема | Решение | |
| 382 | |------|---------| |
| 383 | | Fenced vs anchor | Разные ADR: [0129](0129-intercom-message-body-markdown-and-fenced-code.md) vs этот | |
| 384 | | `[` в fenced code | Attach-грамматика **не** применяется — [0129](0129-intercom-message-body-markdown-and-fenced-code.md) §5 | |
| 385 | | `[` в prose | Только распознаваемые attach-токены; `\[` — фаза 2+ | |
| 386 | | Reveal highlight v1 | Transient **overlay / gutter band** (~3 s), не `Selection`; editor adornment — v2 | |
| 387 | | `memberKey` v1 | `file` + Roslyn **display/qualified name**; rename → stale + re-resolve | |
| 388 | | `memberKey` v2 | DocumentId / symbol id при необходимости | |
| 389 | | Excerpt лимит | **120 строк** или **16 KiB**, хвост `…` | |
| 390 | | Redaction внешний контур | Отдельное событие / политика [0080](0080-intercom-naming-and-multi-party-channel-model.md) | |
| 391 | | `@file` inline | **Отложено** до фазы 2 attach | |
| 392 | | Нет файла у получателя | Excerpt + warning; reveal **не** открывает пустой файл — §9.1 | |
| 393 | | Ветка @ send | `senderWorkspaceContext` на сообщении — подсказка UI/агенту; checkout только явно — §3.1 | |
| 394 | | Anchor CLI | `anchors list` + `/anchor peek` в **CCL**; id в UI — hover vs inline — §10.1 | |
| 395 | |
| 396 | --- |
| 397 | |
| 398 | ## Открытые вопросы |
| 399 | |
| 400 | 1. Точная JSON-schema `AttachmentAnchor` + `senderWorkspaceContext` в [0045](0045-agent-chat-persistence-event-log-and-projections.md) (`schema_version` bump). |
| 401 | 2. Нужен ли **отдельный** `command_id` «скопировать excerpt anchor» vs общий copy selection. |
| 402 | 3. Паритет: агент шлёт только fenced code без anchor — когда UI предлагает «attach как ссылку на файл» (heuristic, v2+; координация с [0129](0129-intercom-message-body-markdown-and-fenced-code.md)). |
| 403 | |
| 404 | --- |
| 405 | |
| 406 | ## История |
| 407 | |
| 408 | | Дата | Изменение | |
| 409 | |------|-----------| |
| 410 | | 2026-05-19 | Proposed: канон AttachmentAnchor, H/M слои, `/attach`, reveal, re-resolve. | |
| 411 | | 2026-05-19 | §11–12 fenced/MD → [0129](0129-intercom-message-body-markdown-and-fenced-code.md); attach-only scope. | |
| 412 | | 2026-05-19 | §9.1 исходы re-resolve: другая ветка / нет файла; excerpt как общий знаменатель. | |
| 413 | | 2026-05-19 | §3.1 `senderWorkspaceContext` (ветка, commit) на уровне сообщения @ send. | |
| 414 | | 2026-05-19 | §5.1 оси bracket `F` \| `M` \| `L` \| `S` (scope/statement); `S:for:n`, не `B:`; L2 с `S:`. | |
| 415 | | 2026-05-20 | **Accepted · In progress**; колонка CIDE в фазах; связка с [0130](0130-editor-agent-range-reveal-without-selection.md) / [0131](0131-editor-slash-select-code-by-bracket-reference.md) в §8. | |
| 416 | | 2026-05-21 | §10.1 anchor id в CCL (`anchors list`, `/anchor peek`); связь с [0138](0138-cockpit-command-line-and-parametric-ranges.md) | |
| 417 | | 2026-05-20 | **Accepted · Implemented**; фаза 2: composer bracket autocomplete (`ChatBracketAutocomplete`, общий popup с slash); фаза 4 — только structural picker. | |
| 418 | |