27f349550c
- sketches/index.html: все экраны одним документом в порядке 1·… (14–19 — бывшие approved) - approved/ (4 файла групп + 3 макета + README) удалён как дубль - стили вынесены в sketches/style.css - README, THEMES, BORROW-FROM-ASSISTENT, MARKDOWN-SOURCE, MIC-ASR-SEARCH приведены к фактическому состоянию
154 lines
12 KiB
Markdown
154 lines
12 KiB
Markdown
# 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`; выбор движка/модели
|
||
в настройках не сделан.
|
||
|
||
## Как запустить
|
||
|
||
```bash
|
||
./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. Где показывать несколько агентов в одном списке диалогов (сейчас список —
|
||
у активного агента).
|