| 1 | # ADR 0123: Intercom — эволюция к full-Skia surface (плотный Slack/MM-like UX) |
| 2 | |
| 3 | **Статус:** Accepted |
| 4 | **Дата:** 2026-05-18 |
| 5 | |
| 6 | ## Связанные ADR |
| 7 | |
| 8 | | ADR | Роль | |
| 9 | |-----|------| |
| 10 | | [0044](0044-avalonia-host-skia-agent-chat-surface.md) | Хост Avalonia + Skia для ленты; модель первична; без «весь чат Skia одним махом» | |
| 11 | | [0057](0057-chat-surface-pipeline-adoption.md) | Pipeline snapshot → entities → layout → render | |
| 12 | | [0117](0117-ide-skia-kit.md) | `Views/SkiaKit/` — примитивы; чат собирает сцену из kit | |
| 13 | | [0119](0119-chat-slash-commands-intercom-surface.md) | Слэши и autocomplete в `ChatInput` | |
| 14 | | [0120](0120-primary-work-surface-intercom-or-editor.md) | Intercom в Forward — якорь внимания; компактный chrome | |
| 15 | | [0121](0121-intent-oriented-programming-paradigm.md) | Intercom — центр вокруг цели; структура важнее «ленты ради ленты» | |
| 16 | | [0126](0126-intercom-inspect-slash-and-compact-chrome-status.md) | Compact: статус в toolbar; навигационный хвост убран из скролла | |
| 17 | | [0031](0031-agent-chat-clarification-batches-and-threading.md) | Пакеты уточнений — структурированный UI | |
| 18 | | [0066](0066-cockpit-ui-vs-ide-presentation-layer.md) | Cockpit UI vs IDE chrome; Intercom — **не** «ещё один Avalonia-экран» | |
| 19 | | [0076](0076-ui-ux-principles-hub.md) | Плотность, токены; **UiKit** — shell/MFD-настройки, не лента Intercom | |
| 20 | | [0072](0072-chat-topic-cards-intent-melody-keyboard-contract.md) | Topic cards, overview/detail | |
| 21 | | [0098](0098-semantic-first-document-as-projection.md) | Редактор кода — тяжёлый контур, остаётся на AvaloniaEdit | |
| 22 | |
| 23 | ### Вне ADR |
| 24 | |
| 25 | | Документ | Роль | |
| 26 | |----------|------| |
| 27 | | [feature-archetype-v1.md](../design/feature-archetype-v1.md) | Skia-surface: Intent → Layout → Render; примитивы из SkiaKit | |
| 28 | | [ide-chrome-tokens-v1.md](../design/ide-chrome-tokens-v1.md) | Токены Avalonia chrome; мост в `SkiaChatTheme` | |
| 29 | | [iop-manifest-v1.md](../iop-manifest-v1.md) | Intercom как hub коммуникации | |
| 30 | |
| 31 | --- |
| 32 | |
| 33 | ## Резюме |
| 34 | |
| 35 | - **Принцип (согласовано с [0044](0044-avalonia-host-skia-agent-chat-surface.md)):** **Avalonia — только фюзеляж** (окно, зоны PFD/Forward/MFD, lifecycle, DPI). **Intercom** — **наш** Skia-surface (лента, composer, slash, toolbar). Стандартные Avalonia-контролы в чате (`TextBox`, `ListBox`, `CascadeSection` в ленте) — **не целевое состояние**; визуально они выбиваются из продукта («из-под топора») и противоречат разделению слоёв [0066](0066-cockpit-ui-vs-ide-presentation-layer.md). |
| 36 | - **На Avalonia остаются «тяжёлые» контуры:** прежде всего **редактор кода** (AvaloniaEdit), плюс узкий класс **IDS-оверлеев** с богатыми формами (пакеты уточнений [0031](0031-agent-chat-clarification-batches-and-threading.md), модалки настроек) — не как постоянная «оболочка чата». |
| 37 | - **Целевая картина:** один control **`IntercomSkiaSurface`** (эволюция `SkiaChatSurfaceControl`) — лента + composer + slash popup; Slack/MM-like плотность. |
| 38 | - **Strangler, не big-bang:** текущий `ChatPanelView` с Avalonia — **временный мост**, подлежит сужению до пустого `Panel`-хоста. |
| 39 | - **Код v0 (2026-05-18):** compact layout на Avalonia — **тактический** компромисс до фазы 1–2; **не** эталон UX. |
| 40 | |
| 41 | --- |
| 42 | |
| 43 | ## Контекст |
| 44 | |
| 45 | После [0120](0120-primary-work-surface-intercom-or-editor.md) Intercom может занимать **Forward**. Пользователь ожидает опыт уровня **Cursor / Slack / Mattermost**: одна вертикальная «колонна разговора», минимум рамок, максимум смысла на пиксель. |
| 46 | |
| 47 | **Сейчас в коде:** |
| 48 | |
| 49 | | Слой | Технология | Содержимое | |
| 50 | |------|------------|------------| |
| 51 | | Лента, topic cards, spine strip | **Skia** (`SkiaChatSurfaceControl`, entity pipeline) | Сообщения, overview, ветки | |
| 52 | | Шапка, spine-редактор, уточнения | **Avalonia** (`ChatPanelView`, UiKit) — **долг** | Секции, TextBox, ListBox | |
| 53 | | Ввод + slash autocomplete | **Avalonia** — **долг** | [0119](0119-chat-slash-commands-intercom-surface.md) | |
| 54 | |
| 55 | Два визуальных «мира» в одной панели: Skia выглядит как продукт, Avalonia-остров — как чужой Fluent-виджет. При `primary_work_surface = intercom` это блокирует позиционирование Intercom как центра сессии ([0121](0121-intent-oriented-programming-paradigm.md)). |
| 56 | |
| 57 | --- |
| 58 | |
| 59 | ## Проблема |
| 60 | |
| 61 | 1. **Чужой chrome:** стили `cascadeSection` / Fluent `TextBox` не дают плотного «мессенджерного» ритма; полировка UiKit **не** решает — нужен **свой** render-слой. |
| 62 | 2. **Нарушение [0044](0044-avalonia-host-skia-agent-chat-surface.md):** в зоне агента снова вырастает полноценный Avalonia-UI вместо «фюзеляж + прибор». |
| 63 | 3. **Два скролла / два ритма:** лента Skia vs composer Avalonia — нет **единой колонны**. |
| 64 | 4. **Ограничение роста:** группировка, meta-строки, inline code, slash-popup — в Skia; наращивание Avalonia-контролов — тупик. |
| 65 | 5. **Риск big-bang:** IME/ввод/доступность — инженерная задача фазы 2, а не повод оставлять `TextBox` навсегда ([0044](0044-avalonia-host-skia-agent-chat-surface.md) отверг только «одним махом без модели», не Skia-composer как цель). |
| 66 | |
| 67 | --- |
| 68 | |
| 69 | ## Целевое UX (ориентиры Slack / MM) |
| 70 | |
| 71 | Не копировать пиксель-в-пиксель, а зафиксировать **инварианты плотности**: |
| 72 | |
| 73 | | Инвариант | Смысл | |
| 74 | |-----------|--------| |
| 75 | | **Одна колонна** | Вертикальный поток: (опционально) toolbar → лента → composer; один scroll для истории | |
| 76 | | **Роли визуально различимы** | user / assistant / system / tool — фон, выравнивание или полоска, без «простыни одного стиля» | |
| 77 | | **Meta одной строкой** | Автор · время · ветка — 11–12px, приглушённый цвет; тело 13–14px | |
| 78 | | **Группировка** | Подряд идущие сообщения одного автора — без повторения заголовка (фаза 2+) | |
| 79 | | **Composer всегда снизу** | Фиксированная зона ввода; slash-list **над** полем, не отдельная «карточка» | |
| 80 | | **Overview не ломает колонну** | Картотека тем — режим той же поверхности (уже есть `OverviewMode`), не отдельная «страница Avalonia» | |
| 81 | |
| 82 | **Spine:** в Forward — **только** компактная Skia-полоска ([0096](0096-intercom-topic-card-summary-and-product-spine.md)); полный редактор spine — MFD / палитра / modal (IDS), не Expander в ленте. |
| 83 | |
| 84 | --- |
| 85 | |
| 86 | ## Решение |
| 87 | |
| 88 | <a id="adr0123-p0"></a> |
| 89 | |
| 90 | ### Принцип: фюзеляж vs наш surface |
| 91 | |
| 92 | | Слой | Технология | Примеры | |
| 93 | |------|------------|---------| |
| 94 | | **Фюзеляж** | Avalonia | `MainWindow`, `AttentionZoneContainer`, splitters, `MfdHostWindow`, **пустой** host `Panel` для Intercom | |
| 95 | | **Тяжёлый контент** | Avalonia + специализированные контролы | **AvaloniaEdit**, Terminal, Git diff views, Rich settings | |
| 96 | | **Intercom (цель)** | **Skia** (+ SkiaKit) | Лента, topic cards, composer, slash popup, toolbar chip | |
| 97 | | **IDS / редкие формы** | Avalonia overlay **поверх** Skia | Clarification batch, мастера; не встроенные `Expander` в ленте | |
| 98 | |
| 99 | **Запрещено как steady state:** `ChatPanelView` с `CascadeSection` + `TextBox` + `ListBox` как основной UX Intercom. |
| 100 | |
| 101 | <a id="adr0123-p1"></a> |
| 102 | |
| 103 | ### Поэтапный strangler |
| 104 | |
| 105 | ### Фаза 0 — тактический долг (2026-05-18) |
| 106 | |
| 107 | - `primary_work_surface = intercom`, `SkiaChatDensity` / `CompactLayout`. |
| 108 | - Урезанный Avalonia chrome — **временно**, до выноса toolbar/composer в Skia. |
| 109 | |
| 110 | ### Фаза 1 — **Единая Skia-колонна (лента + chrome)** |
| 111 | |
| 112 | `IntercomSkiaSurface` (rename от `SkiaChatSurfaceControl`) на **весь** клиент зоны; `ChatPanelView` → `Content="{Binding …}"` одного control или `Panel` без дочерних Avalonia-виджетов: |
| 113 | |
| 114 | - Toolbar, chip загрузки, кнопка overview — **рисуются в Skia** (или одна строка в layout engine). |
| 115 | - Никаких `CascadeSection` / `Expander` spine в Forward. |
| 116 | - Один вертикальный scroll истории внутри surface. |
| 117 | |
| 118 | **Критерий:** в дереве визуала под Forward для Intercom — **нет** `TextBox`/`ListBox`/`Button` Fluent-стиля (кроме будущего IDS-overlay). |
| 119 | |
| 120 | ### Фаза 2 — **Skia composer + slash (целевой ввод)** |
| 121 | |
| 122 | - Composer: Skia-рамка, placeholder, кнопка send, многострочный рост по содержимому. |
| 123 | - **Ввод текста:** **`ITextInputMethodClient`** на Avalonia **12** (`TopLevel.TextInputMethod.SetClient`) + отрисовка composer в Skia из `ChatInput` (preedit через `SetPreeditText`). **Без** видимого Fluent `TextBox` в steady state. |
| 124 | - Slash: `SkiaPopupList` (SkiaKit), hit-test; данные из `ChatSlashAutocomplete` ([0119](0119-chat-slash-commands-intercom-surface.md)). |
| 125 | - Клавиши: `ChatSendKeyMatcher`, Tab/↑/↓/Esc — на `IntercomSkiaSurface`. |
| 126 | |
| 127 | **Критерий:** пользователь не видит «чужих» Avalonia-контролов в Intercom; ввод не хуже нативного по кириллице/IME на Windows (ручной чеклист). |
| 128 | |
| 129 | **Отклонено для steady state:** «прозрачный `TextBox` поверх Skia» — оставляет Fluent-семантику и дублирует фюзеляжный UI в продуктовой зоне. |
| 130 | |
| 131 | ### Фаза 3 — **Богатая лента (ёмкость)** |
| 132 | |
| 133 | - Группировка сообщений, схлопнутые блоки кода (mono strip), сворачиваемый «thinking». |
| 134 | - ~~Markdown subset в Skia~~ → **v1.1:** `SkiaMarkdownLayout` в prose; fenced — `ChatMessageBodyPresentation` — канон [0129](0129-intercom-message-body-markdown-and-fenced-code.md). |
| 135 | - ~~Копирование выделения~~ → **v1.1:** выбранное сообщение + Ctrl+C → clipboard (`ChatSurfaceSnapshotMessageLookup`). |
| 136 | |
| 137 | ### Фаза 4 — **Только тяжёлые IDS-оверлеи на Avalonia** |
| 138 | |
| 139 | - **Clarification batch** ([0031](0031-agent-chat-clarification-batches-and-threading.md)) — отдельный **overlay** / flyout (IDS), не секция в `ChatPanelView`; допустимы Avalonia-формы как «тяжёлый» контур. |
| 140 | - **AI settings**, **Terminal**, **Editor** — без изменений по технологии. |
| 141 | - **MFD → Chat page:** после стабилизации Forward — **тот же** `IntercomSkiaSurface` (comfortable density), **не** отдельный UiKit-экран. |
| 142 | |
| 143 | <a id="adr0123-p2"></a> |
| 144 | |
| 145 | ### Архитектурные границы (не менять) |
| 146 | |
| 147 | ``` |
| 148 | ChatSurfaceSnapshot (Features/Chat, CCU) |
| 149 | ↓ |
| 150 | ChatSurfaceEntityFactory → ISkiaChatEntity[] |
| 151 | ↓ |
| 152 | SkiaChatLayoutEngine → SkiaChatPlacedEntity[] |
| 153 | ↓ |
| 154 | Skia draw (Views/Chat/Skia, SkiaKit) |
| 155 | ``` |
| 156 | |
| 157 | - ViewModel **не** импортируется из Skia-слоя ([0117](0117-ide-skia-kit.md)). |
| 158 | - Новые примитивы composer/slash — сначала **SkiaKit** (например `SkiaComposerStrip`, `SkiaPopupList`), затем использование в чате. |
| 159 | - Токены: `SkiaChatTheme` / `SkiaKitThemeBridge` ← `CascadeTheme.*` ([ide-chrome-tokens-v1.md](../design/ide-chrome-tokens-v1.md)). |
| 160 | |
| 161 | ### Плотность (один surface, два пресета) |
| 162 | |
| 163 | | Пресет | Когда | Метрики | |
| 164 | |--------|-------|---------| |
| 165 | | **Compact** | Forward + `primary_work_surface = intercom` | `SkiaChatDensity` | |
| 166 | | **Comfortable** | MFD Chat, узкая колонка | чуть больше padding, те же примитивы | |
| 167 | |
| 168 | Один `IntercomSkiaSurface`, без второго Avalonia-макета. |
| 169 | |
| 170 | --- |
| 171 | |
| 172 | ## Отклонённые альтернативы |
| 173 | |
| 174 | | Альтернатива | Почему нет | |
| 175 | |--------------|------------| |
| 176 | | **Дальше полировать UiKit в чате** | Не достигает Slack/MM плотности; закрепляет «два мира»; против [0066](0066-cockpit-ui-vs-ide-presentation-layer.md) | |
| 177 | | **Прозрачный Avalonia `TextBox` в composer** | Всё ещё Fluent-контрол в продуктовой зоне; против принципа фюзеляжа | |
| 178 | | **Всё на Avalonia** `ItemsControl` | Против [0044](0044-avalonia-host-skia-agent-chat-surface.md) | |
| 179 | | **Full-Skia для clarification forms** | Слишком дорого; оверлей IDS на Avalonia — ок | |
| 180 | | **Big-bang без фаз 1–2** | Риск IME; [0044](0044-avalonia-host-skia-agent-chat-surface.md) | |
| 181 | | **Отдельное окно чата** | Ломает [0017](0017-multi-window-workspace-and-agent-surfaces.md), [0120](0120-primary-work-surface-intercom-or-editor.md) | |
| 182 | |
| 183 | --- |
| 184 | |
| 185 | ## Последствия |
| 186 | |
| 187 | - **Плюсы:** единый визуальный язык, плотность, проще анимации и hit-test; лучше стык с semantic/graph surfaces ([0067](0067-graph-backed-surfaces-contract.md)). |
| 188 | - **Минусы:** две реализации composer (compact/comfortable) до схождения; тесты UI сложнее — опора на snapshot/layout unit tests + немного headless Skia bounds tests. |
| 189 | - **MCP / агент:** без изменений wire; `ChatInput` для MCP может оставаться VM-строкой, UI — проекция. |
| 190 | |
| 191 | --- |
| 192 | |
| 193 | ## Метрики успеха (для принятия Implemented) |
| 194 | |
| 195 | 1. Forward + intercom: **один** визуальный scroll-контур для истории. |
| 196 | 2. В Intercom **нет** видимых Avalonia `TextBox` / `ListBox` / `CascadeSection` (кроме IDS-overlay уточнений). |
| 197 | 3. Плотность: ≥ на 20% больше видимых строк истории при 1080p на той же истории (эвристика, сравнение до/после скриншота). |
| 198 | 4. Регрессии: [0119](0119-chat-slash-commands-intercom-surface.md) autocomplete, Enter-send, overview/detail — зелёные тесты/ручной чеклист. |
| 199 | |
| 200 | --- |
| 201 | |
| 202 | ## Открытые вопросы |
| 203 | |
| 204 | 1. **Platform input:** `InputMethod` + offscreen presenter vs отдельный нативный HWND для composer на Windows — спайк в фазе 2. |
| 205 | 2. *(закрыто — [0129](0129-intercom-message-body-markdown-and-fenced-code.md))* лента: Skia subset + fenced; полный MD — preview [0069](0069-markdown-preview-tool-surface-and-renderer-decoupling.md) по действию на сообщении. |
| 206 | 3. **Clarifications IDS:** bottom sheet vs боковая панель (оба — Avalonia overlay, не встроенный блок). |
| 207 | 4. **Приоритет:** фаза 1 (убрать Avalonia-остров) перед фазой 3 (группировка сообщений) — **рекомендуется**. |
| 208 | |
| 209 | --- |
| 210 | |
| 211 | ## История изменений |
| 212 | |
| 213 | | Дата | Изменение | |
| 214 | |------|-----------| |
| 215 | | 2026-05-18 | Proposed: full-Skia evolution, фазы 0–4, открытые вопросы. | |
| 216 | | 2026-05-18 | Уточнение: Avalonia только фюзеляж + тяжёлые контуры; отказ от steady-state Avalonia в Intercom; composer через `ITextInputMethodClient` (Avalonia 12), не Fluent TextBox. | |
| 217 | | 2026-05-18 | **Accepted.** Фаза 1 в коде: Forward shell, Skia toolbar, `intercomComposer` strip (временный TextBox до фазы 2). | |
| 218 | | 2026-05-17 | **Implemented (фазы 1–3 v1):** `IntercomSkiaSurface`, Skia composer + `SkiaPopupList` + IME client; MFD на том же surface; группировка ленты, `SkiaMonoCodeStrip`, double-click thinking toggle. Markdown subset и copy — открыто. | |
| 219 | | 2026-05-18 | **Render fix:** offscreen `WriteableBitmap` + `DrawImage` (не `SKCanvas.Clear` на leased canvas окна — [Avalonia #5932](https://github.com/AvaloniaUI/Avalonia/issues/5932)). | |
| 220 | | 2026-05-18 | **Фаза 3 (v1.1):** inline Markdown subset (`**` / `*` / `` ` ``) в prose; Ctrl+C копирует тело выбранного сообщения; MFD Chat на `IsSkiaIntercomHostVisible` (comfortable `CompactLayout=false`). | |
| 221 | | 2026-05-20 | **Flat feed в ленте:** `SkiaChatBubbleKind.Feed` — user/agent/thinking/tool, slash outcome, clarification, nav-строки без messenger-пузыря; акцент — selection / branch / ошибка. | |