subochev 2c71542b13 Десктоп-клиент: Compose-UI, сессии, SQLite-кэш, голос, живые тесты
- settings/: settings.json (агенты, тема, clientId), атомарная запись
- persistence/: ConversationMetaRepository — группы, lastSeen, превью,
  watermark синхронизации (synced_at через PRAGMA table_info)
- session/: AgentRegistry (параллельное подключение, деградация в офлайн),
  AgentConnection (журнал + мета + живой список диалогов conversationsFlow,
  refresh по SSE-потоку), ChatSession (history + backfill + live + preview,
  дедуп по id, 1 мс шаг watermark, syncMutex), LiveTurn
- UI: рейка агентов, список диалогов (живое обновление), чат со стримингом,
  композер (Enter отправляет, Shift+Enter переносит, Ctrl+M — голос),
  окна «Агенты»/«Группы»/«Новый диалог», тёмная и светлая темы
- voice/: mic-api → Silero VAD → Vosk на одном выделенном потоке;
  VoiceNative.warmUp() до skia (иначе SIGSEGV), модель в артефакте
- markdown/: вендоренный рендер из ai/assistent
- ошибки в баннер через AppState.reportError (CancellationException игнорируется)
- тесты 103 (реальная SQLite + фейковый транспорт); против живого агента —
  RealServerIT, RealAppStateIT, RealSpeechIT
- LIVE-RUN-NOTES.md: живой прогон в GUI и найденные/починенные баги
2026-09-28 10:08:10 +03:00

agentik-desktop — десктопный клиент агента agentik

Клиент агента agentik на Kotlin Multiplatform + Compose Desktop (цель сборки — только JVM). UI общается с агентом через готовую библиотеку pw.binom.agentik:client: она даёт Agent / Conversation, живой поток событий (SSE), авто-reconnect, кэш списка диалогов и interrupt() для кнопки «Стоп».

Проект уже собирается и запускается (./gradlew run), а не только спроектирован.

Что где лежит

sketches/index.html     # ВСЕ экраны одним документом: список диалогов, чат, запись,
                        # группы, агенты, проверка связи, пустые состояния.
                        # Это и черновик, и «одобренное» — отдельного approved/ больше нет.
sketches/style.css      # палитра и стили макетов (цвета числами — так и надо, это макет)
src/jvmMain/kotlin/...  # код приложения
src/jvmTest/kotlin/...  # тесты

Документы рядом:

  • REQUIREMENTS.md — требования (накиданы, ни один пункт не обязателен в текущей форме).
  • STORAGE.md — что и где хранится на клиенте (SQLite + JSON).
  • CACHE.md — кэш сообщений: что есть в библиотеке, схема «только новое».
  • THEMES.md — цвета не пишем в коде, только из семантической схемы.
  • MARKDOWN-SOURCE.md — откуда взята отрисовка Markdown.
  • BORROW-FROM-ASSISTENT.md — что берём из ai/assistent, а что нет.
  • MIC-ASR-SEARCH.md — библиотеки микрофона/распознавания (mic-kmp, asr-kmp, vad-kmp).

Структура кода

pw.binom.agentik.desktop
├── Main.kt                      # точка входа: окно, реестр, состояние
├── settings/                    # AppPaths, AppSettings/AgentConfig, SettingsRepository
├── persistence/                 # ConversationMetaRepository (группы/lastSeen/preview/watermark)
├── session/                     # LiveTurn, ChatSession, AgentConnection, AgentRegistry
├── model/                       # UiModels + Mappers (журнал/стрим → UI)
├── media/                       # AvatarStorage (копия картинки агента)
├── net/                         # ConnectionChecker (проверка связи)
├── markdown/                    # вендоренный парсер Markdown (из assistent)
└── ui/                          # App, AppState, theme/, components/, screens/

Ключевые решения по слоям — в STORAGE.md; кратко:

  • Транспорт целиком в :client. HTTP/JSON/SSE/Bearer, reconnect, кэш списка диалогов — не переписываем.
  • Сообщения — KsqliteJournalStore из :journal-ksqlite (offline-история).
  • Группы, lastSeen, превью, watermark синхронизации — наш ConversationMetaRepository (сервер про это не знает).
  • Отдельная SQLite-база на агента: id диалогов серверные, у двух агентов могут совпасть; плюс группы локальны для агента.

Текущее состояние реализации

Готово и покрыто тестами:

  • Настройки — settings.json (агенты, тема, выбранная группа, последний диалог), clientId генерируется один раз. Атомарная запись.
  • Живой список диалогов — AgentConnection.conversationsFlow опрашивает встроенный кэш либы (conversationStore.listFlow, warmup-цикл на старте) и дополнительно подписан на SSE-поток агента: диалог, созданный с другого клиента (или самим сервером), появляется в списке сам; свои создания и удаления отражаются сразу. Ошибки сервера не считаются «пустым списком» — последний известный список сохраняется.
  • Реестр агентов — добавление/правка/удаление, подключение, деградация в «не отвечает» без падения приложения.
  • Стриминг — LiveTurn применяет события Working → StartReasoning → AppendText → ToolCall/ToolResult → End/Interrupted/Error; ChatSession догоняет историю (after = synced_at), пишет в локальный кэш, склеивает live + history, обновляет превью и lastSeen.
  • История/UI-модели — мапперы MessageRecord → UiMessage со склейкой ToolCall+ToolResult в одну карточку.
  • Группы — CRUD групп + раскладка диалогов (каскад: удаление группы не удаляет диалоги).
  • Тема — семантические цвета (AgentikTheme.colors), тёмная + светлая схемы; отдельно пользовательские цвета агентов.
  • Markdown — вендоренный рендер из ai/assistent (см. MARKDOWN-SOURCE.md).
  • Аватары — копирование выбранного файла в ~/.agentik-desktop/avatars/.
  • Проверка связи — «ответ сервера» + создание/удаление временного диалога, с таймингами.
  • UI (Compose) — рейка агентов, список диалогов (поиск, папки, бейджи), чат с живым стримингом, композер, окна «Агенты», «Группы» и «Новый диалог» (при нескольких агентах — выбор, при одном — сразу), пустые состояния, адаптивная раскладка (узкое окно — список или чат).
  • Голос — микрофон → VAD → распознавание (voice/): кнопка микрофона или Ctrl+M из чата запускает запись (mic-api), Silero VAD (vad-kmp) отмечает наличие речи, Vosk (asr-vosk, модель внутри артефакта) отдаёт промежуточный текст над полем ввода, финал дописывается в черновик. Тишина текстом не становится. Нативные модели считаются на одном выделенном потоке. На старте main() делает [VoiceNative.warmUp()] — обязательный прогрев Vosk/ONNX ДО инициализации skia/Compose (иначе создание модели после отрисовки окна роняет JVM в C++-логах; подробности в MIC-ASR-SEARCH.md).
  • Реальная интеграция — интеграционные тесты гоняются против живого :standalone-агента (Qwen через vLLM), а не только in-memory-фейков: RealServerIT (диалог → отправка → живой ответ LLM → локальный кэш) и RealAppStateIT (весь AppState: реестр → подключение → живой список → создание диалога → стриминг → превью/lastSeen), RealSpeechIT (речь: прогрев нативных ASR → инициализация Compose → распознавание реального wav → «раз два три четыре пять»). См. AGENTIK_E2E_URL.

Проверено вживую (окно приложения + живой агент): список диалогов подтягивается с сервера, кнопка «+» создаёт диалог, сообщение уходит и ответ приходит в UI, Ctrl+M переводит микрофон в запись и обратно без падения JVM.

Не начато / заглушки:

  • Свёрнутый вид хода работы агента — сейчас живой ход показывается развёрнуто (reasoning, карточки ToolCall/ToolResult, спиннер). Как его складывать — вопрос дизайна, не решён (REQUIREMENTS §9).
  • Кусок прерванного ответа в кэше — R39 выполнен (прерванный ответ в историю не пишется), а вот хранить ли его обрывок в кэше — вопрос не решён (REQUIREMENTS §9).
  • Светлая тема визуально не проверялась глазами (THEMES.md); в ui/ThemeRenderTest.kt проверено программно: фон код-блоков берётся из слота темы, порядок яркостей в светлой палитре корректный.
  • Голос: движок зашит на Vosk (VoskVoiceRecognizer) — Qwen3/Whisper из asr-kmp подключаются заменой VoiceRecognizerFactory; выбор движка/модели в настройках не сделан.

Как запустить

./gradlew run         # окно приложения
./gradlew jvmTest     # тесты (реальная SQLite, фейковый транспорт)

# Интеграционные тесты против живого агента (по умолчанию пропускаются):
AGENTIK_E2E_URL=http://127.0.0.1:8080/agentik ./gradlew jvmTest \
  --tests '*RealServerIT' --tests '*RealAppStateIT'
./gradlew jvmJar      # собрать jar

Что решено

  • Сначала десктоп, затем Android — по тем же лекалам. Десктоп делаем образцом: понятный и читаемый по коду. Потом Android повторяет решения.
  • Несколько агентов — различение цветом или картинкой (выбирает пользователь). Картинку клиент копирует себе, а не берёт по адресу.
  • Markdown — показывать разметкой. Готовый рендер взят из ai/assistent.
  • Папки над списком диалогов — сохранены.
  • Проверка связи — отдельным окном, с таймингами шагов.
  • Ориентир по размеру: ~9 небольших кусков логики поверх библиотеки, а не собственная инфраструктура (см. STORAGE.md, CACHE.md).

Что предстоит решить

  1. Запись — по нажатию или на удержание.
  2. Форма показа хода работы агента в свёрнутом виде.
  3. Различение агентов: одного цвета может оказаться мало.
  4. Что делать с куском прерванного ответа в кэше (CACHE.md, п. 3.2).
  5. Где показывать несколько агентов в одном списке диалогов (сейчас список — у активного агента).
S
Description
Десктопный клиент агента agentik (Jetpack Compose Desktop). Ходит в :client-библиотеку. Голосовое управление.
Readme 1.1 MiB
Languages
Kotlin 78.6%
HTML 15.3%
CSS 6.1%