# Что берём из `ai/assistent`, а что не берём **Правило:** из `ai/assistent` берём **только приёмы отрисовки, плагины и версии**. Архитектуру, слои, окна и дизайн — **не берём**, пишем своё. Дизайн у нас свой, нарисованный (см. `sketches/`), и он не должен наследовать чужую структуру. **Репозиторий:** https://git.binom.pw/ai/assistent, ветка `main`, коммит `2c5b3f9` **RAW-адреса** (нужен логин, репозиторий приватный): `https://git.binom.pw/ai/assistent/raw/branch/main/<путь>` --- # ЧАСТЬ 1. ЧТО БЕРЁМ ## 1. Плагины и версии Из `gradle/libs.versions.toml` и `gradle/wrapper/` репозитория assistent: | Что | Версия | |---|---| | Kotlin | `2.4.0` | | Compose Multiplatform | `1.7.3` | | kotlinx-serialization | `1.11.0` | | kotlinx-coroutines | `1.11.0` | | Ktor | `3.1.2` | | kotlin-logging | `7.0.3` | | ktlint | `14.2.0` | | JUnit | `5.10.2` | | Mockito | `5.12.0` | | log4j | `2.24.3` | | Gradle | `9.4.1` | **Плагины** (модуль десктоп-клиента, `client/build.gradle.kts`): ```kotlin plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.compose) alias(libs.plugins.ktlint) } ``` **Зависимости Compose для десктопа:** ```kotlin implementation(compose.desktop.currentOs) implementation(compose.material3) implementation(compose.materialIconsExtended) implementation(libs.kotlinx.coroutines.swing) // ВАЖНО: Dispatchers.Main на десктопе = Swing EDT ``` **Репозитории** (без них не соберётся): ```kotlin mavenCentral() mavenLocal() maven { url = uri("http://192.168.76.117/repository/developerspace-prod-mvn-hosted/") isAllowInsecureProtocol = true } maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") maven("https://maven.google.com") ``` **Точка входа приложения:** ```kotlin compose.desktop { application { mainClass = "<наш пакет>.MainKt" } } ``` > ⚠️ **Не путать:** в самом agentik версия Kotlin `2.4.20`, а в assistent `2.4.0`. > Ориентироваться на версию нашего проекта agentik, а не копировать слепо. ## 2. Отрисовка Markdown **Уже скопировано в проект** (28.09.2026): 6 файлов + 11 тестов парсера, пакеты переименованы под `pw.binom.agentik.desktop.*`. Подробности, карта «источник → назначение», зависимости и как пересинхронизировать — в **`MARKDOWN-SOURCE.md`**. ## 3. Приёмы отрисовки (самое ценное) Здесь чужие грабли, уже набитые шишки. Каждый пункт — из кода assistent, с указанием места. ### 3.1. Таблица и бесконечная ширина — Compose падает **Где:** `client-shared/.../ui/MarkdownRendering.kt`, функция `renderTable`, строки ~170–258. Нельзя класть `Box(horizontalScroll)` внутрь контейнера с бесконечной максимальной шириной (`wrapContentWidth`) — Compose падает с ошибкой `measured with an infinity maximum width`. Из комментария в коде: - если внешний скролл уже есть — таблица растёт по содержимому (`wrapContentWidth`), а широкие колонки прокручивает **внешний** скролл; - если внешнего нет — скролл добавляется **внутрь** таблицы (`fillMaxWidth` + `horizontalScroll`). Приём: **параметр `isBubbleScrolled`**, которым таблица переключается между двумя режимами, вместо одной универсальной раскладки. ### 3.2. `derivedStateOf` создаёт гонку — читать напрямую **Где:** `client/.../ui/ChatScreen.kt`, строки ~1085–1092. Из комментария: производное состояние, созданное при рекомпозиции, читается observer'ом во время фоновых изменений списков — известная гонка Compose `state created after snapshot was taken`. **Приём:** вместо `derivedStateOf { listState.canScrollForward }` писать просто ```kotlin val isNotAtBottom = listState.canScrollForward ``` Прямое чтение убирает `DerivedSnapshotState` и гонку вместе с ним. ### 3.3. Ленивый список: `getOrNull` + стабильный ключ **Где:** `client/.../ui/ChatScreen.kt`, строки ~1030–1043. Список сообщений меняется фоном (подгрузка истории, новые сообщения), поэтому прямой доступ по индексу может выйти за границы и уронить композицию. ```kotlin items( count = messages.size, key = { index -> messages.getOrNull(index)?.id ?: "pos:$index" }, ) { index -> val msg = messages.getOrNull(index) ?: return@items ... } ``` **Приём:** `count` + `key` + `getOrNull` — вместо `items(list)`. Ключ стабилен при изменении списка, лишних перерисовок нет. ### 3.4. Автоскролл вниз при новых сообщениях **Где:** `client/.../ui/ChatScreen.kt`, строки ~861–868. ```kotlin val listState = rememberLazyListState() LaunchedEffect(messages.size, isThinking, isTranscribing.value) { if (messages.isNotEmpty()) listState.scrollToItem(messages.size - 1) } ``` **Приём:** ключи `LaunchedEffect` — то, при изменении чего прокручивать. Плюс «печатает» и «запись голоса» в ключах: прокрутка случается и при появлении индикатора, а не только при новом сообщении. Для нас то же: ответ течёт потоком. ### 3.5. Кнопка «прокрутить вниз» — плавно и без гонки **Где:** там же, строки ~1085–1110. ```kotlin val isNotAtBottom = listState.canScrollForward val buttonAlpha by animateFloatAsState( targetValue = if (isNotAtBottom) 1f else 0f, animationSpec = tween(200), label = "scrollBtnAlpha", ) ``` Прокрутка — в корутине: `scope.launch { listState.animateScrollToItem(size - 1) }`. Кнопка проявляется/исчезает плавно, а не мигает. ### 3.6. Полоса прокрутки, видимая по наведению **Где:** там же, строки ~1062–1086. ```kotlin VerticalScrollbar( modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight() .alpha(msgScrollbarAlpha).width(8.dp), adapter = rememberScrollbarAdapter(scrollState = listState), style = ScrollbarStyle(minimalHeight = 30.dp, thickness = 8.dp, shape = RoundedCornerShape(4.dp), hoverDurationMillis = 150), ) ``` **Приём:** `rememberScrollbarAdapter` привязывает полосу к ленивому списку; ширина 8 dp и появление по наведению — не мозолит глаза. ### 3.7. Кликабельные ссылки внутри текста **Где:** `client-shared/.../ui/MarkdownRendering.kt`, строки ~301–336. Приём: `pointerInput` + `detectTapGestures` даёт координаты нажатия; `onTextLayout` сохраняет `textLayoutResult`; `getOffsetForPosition(offset)` превращает координаты в позицию в тексте; по позиции ищем аннотацию (ссылка или код) и открываем её. ```kotlin Modifier .pointerInput(annotatedString) { detectTapGestures { offset -> val layout = textLayoutResult ?: return@detectTapGestures val position = layout.getOffsetForPosition(offset) ... // найти аннотацию по position } } ``` **Зачем нам:** ссылки в ответах агента должны открываться в браузере, а не быть просто текстом. ### 3.8. Копирование блока кода по кнопке **Где:** там же, строки ~90–125. ```kotlin val clipboardManager = LocalClipboardManager.current ... clipboardManager.setText(AnnotatedString(segment.code)) ``` **Приём:** `LocalClipboardManager` работает и на десктопе, отдельная библиотека не нужна. ### 3.9. Выделение текста мышью `SelectionContainer` вокруг сообщения — иначе текст не выделить, а для агента это нужно постоянно. ### 3.10. Образец темы: ценный, но повторять НЕ всё **Где:** `client-shared/src/main/kotlin/pw/binom/client/shared/ui/FolderTheme.kt`. Там есть готовый образец, **очень близкий к тому, что нужно нам**: ```kotlin data class ClientTheme( val name: String, val colorScheme: ColorScheme, val folderIconColor: Color, val unreadBadgeColor: Color, // ... ) val DarkClientTheme = ClientTheme( name = "dark", colorScheme = darkColorScheme( primary = Color(0xFF6AB2F2), onPrimary = Color.White, surface = Color(0xFF1A1A2E), onSurface = Color(0xFFEBEBEB), background = Color(0xFF121218), onBackground = Color(0xFFE0E0E0), error = Color(0xFFCF6679), // ... ), // ... ) ``` **Что тут ценно — и почти совпадает с нашим решением:** - тема **уже описана отдельным объектом**, а не размазана по экранам; - в комментарии к коду прямо сказано: «Сейчас — одна тёмная тема; **в будущем — переключатель тем (светлая/тёмная)**». То есть к тому же и шли; - значения цветов объявлены **единым источником правды для всех платформ** (десктоп и Android) — ровно наш случай, десктоп как образец для Android; - кроме цветов схемы в теме есть **именованные значения для смыслов** (`unreadBadgeColor`, `folderIconColor`) — то, к чему мы и хотим прийти. **Что повторять НЕ надо:** в остальном коде assistent цвета всё равно вписаны числами — **36 штук** (`Color(0xFF4CAF50)`, `Color(0xFFF131327)` и подобные) прямо в экранах. То есть theme есть, а половина цветов мимо неё. Плюс `folderIconColor` и подобные значения заданы как `Color(0xFF768C9E)` **внутри** объекта темы — это правильное место, но при второй теме их придётся задавать заново руками. **Наш вывод:** берём **идею** (тема = объект, имена по смыслу, единый источник для платформ), но **делаем строже**: ни одного цвета числом вне самой схемы, все именованные значения — в теме, и всё проверяется на второй теме. Подробно — `THEMES.md`. ### 3.11. Сборка стилей текста **Где:** там же, `MarkdownSegment.Text.toAnnotatedString`, строки ~341–377. `AnnotatedString.Builder` + построение `SpanStyle` из простого описания (`bold`/`italic`/`code`/`strikethrough`/`link`). Логику стилей держат в данных, а не в Композе — удобно и переносимо. ## 4. Запись микрофона — код из assistent брать НЕ надо **Где:** `client/src/main/kotlin/pw/binom/client/audio/MicrophoneRecorder.kt` (83 строки). Всё на стандартной Java (`javax.sound.sampled`: `AudioFormat`, `AudioSystem`, `DataLine`, `TargetDataLine`), формат PCM 16 бит / 16 кГц / моно, буфер 4096 байт. **Но копировать это не нужно.** У нас есть своя готовая библиотека **`mic-kmp`** (опубликована, версия `1.0.0`), которая делает то же самое, только лучше: отмена структурная — микрофон освобождается сам через `finally`, без ручного `close()`. Здесь файл полезен **только как источник приёмов**: - подтверждает формат, который ждёт распознавание: **16 кГц, моно, PCM 16 бит, little-endian** (`AudioFormat.Encoding.PCM_SIGNED, sampleRate, 16, 1, 2, sampleRate, false`); - показывает, что сторонние библиотеки для звука не нужны — хватает стандартной Java; - отдаёт данные кусками (`ReceiveChannel`), а не файлом — можно стримить. Подробности и координаты библиотек — `MIC-ASR-SEARCH.md`. --- # ЧАСТЬ 2. ЧТО НЕ БЕРЁМ ## Архитектура и слои — пишем своё Не копировать: - **`client/src/main/kotlin/pw/binom/client/`** целиком — это чужое приложение: `Main.kt`, `viewmodel/ChatViewModel.kt`, `config/AppSettings.kt`, `config/ClientConfig.kt`, `cache/*`. - **`client/.../ui/ChatScreen.kt`** (1903 строки) — экран чата целиком. Оттуда годятся только перечисленные выше приёмы, а не структура. - **`ui/SettingsDialog.kt`** (301) — чужое окно настроек. У нас свой дизайн настроек (`approved/004-settings-agents.html`). - **`ui/NewChatDialog.kt`** (616), **`ui/ParticipantsDialog.kt`** (223) — чужое. - **`ui/MessageBubble.kt`** (789) — пузырь сообщения; тянет `chat-common` и чужие модели. - **Цвета числами в экранах** — в assistent их 36 штук `Color(0xFF…)` прямо в `ChatScreen` и соседних файлах. Тема там есть, но обходится мимо. Нам нужны цвета строго из схемы, см. `THEMES.md`. - **Модули `chat-*`** (`chat-common`, `chat-client`, `chat-cache`, `chat-adapter`, `chat-server-client`) — чужая система чатов. У нас связь с агентом идёт через **`pw.binom.agentik:client`**, совсем другой протокол. **Почему не берём:** у нас другая модель — один клиент к нескольким агентам agentik, а не чат-сервер с комнатами, участниками и адаптерами. Чужая структура тут только запутает. ## Что у нас своё (не из assistent) - Раскладка и дизайн — `approved/001-sidebar-utility.html` (основа) и `sketches/002`…`005` (остальное). - Различение агентов цветом или картинкой. - Проверка связи с агентом отдельным окном. - Поведение узкого окна (список ↔ чат с кнопкой «назад»). - Показ хода работы агента (свёрнутые шаги). - Хранение списка агентов. --- # Итог одной таблицей | Берём | Не берём | |---|---| | Версии и плагины, репозитории | Архитектуру и слои | | Отрисовку Markdown (6 файлов, уже скопированы) | Экраны целиком (`ChatScreen`, `SettingsDialog`, …) | | Приёмы: таблицы, ленивый список, автоскролл, полоса прокрутки | Модули `chat-*` и модели сообщений | | Кликабельные ссылки, копирование кода, выделение текста | `MessageBubble` | | Приём записи звука (формат 16к/моно, `javax.sound`) | `ChatViewModel`, `config/*`, `cache/*` | | Зависимость `ru.otpbank.ai:markdown:0.51.0` | Чужой дизайн и окна | **Микрофон и распознавание — не из assistent.** У нас для этого есть свои опубликованные библиотеки: `mic-kmp`, `asr-kmp`, `vad-kmp`. Разбор — `MIC-ASR-SEARCH.md`.