feat(events): EventBus + domain-модель статусов

- internal/model: единый источник статусов задачи (Status, TraceStatus,
  машина переходов), без зависимости от storage.
- internal/storage: совместимый мост (type Status = model.Status,
  re-export констант) — внешний код не меняется.
- internal/events: шина событий (fan-out, блокирующий Publish с гарантией
  порядка), события задач/трейсов, отдельная логовая шина + LogWriter,
  Publisher/NilPublisher для внедрения в Core/Worker.
- docs/ui-spec.md: спецификация десктопного UI (Fyne).
This commit is contained in:
ki.sagidullin
2026-08-20 01:02:31 +05:00
parent 408d137747
commit 1f7ab9a67e
10 changed files with 764 additions and 54 deletions

160
docs/ui-spec.md Normal file
View File

@@ -0,0 +1,160 @@
# UI-спека Ratatoskr (Fyne Desktop)
Статус: утверждается. Черновик, обсуждаем дальше (архитектура обмена UI <-> Core).
Стек: **Go + Fyne** (свежая стабильная версия). Платформа: **Windows** (Linux пока не поддерживаем).
---
## 1. Цель и роль UI
1. UI — **полноценный инструмент** (не только админ-панель): управляет всем, чем управляет Telegram, и становится **основным** интерфейсом.
2. Один пользователь, один человек (без multi-user / мультисессии).
3. UI должен уметь **всё, что умеет Telegram-интерфейс**, и станет основным; Telegram остаётся как один из каналов.
4. UI — **графическая оболочка самого приложения** (одна программа), не отдельный process.
5. Внутри оболочки запускается **ботик сам**; UI — это лишь ещё одна реализация `chat.Channel`.
6. Запуск: **обычный запуск бинаря** → UI. Режим без UI — только по флагу **`--noui`**.
- Логика: UI включён по умолчанию, headless — опционально (`--noui`).
7. Трей-иконка не нужна.
8. При закрытии окна — **сворачивать** (не завершать процесс). Для полного выхода — **отдельная кнопка** «Завершить», чтобы бот остановился.
9. Конфиг-секции редактировать в UI пока не нужно; **hot-reload конфига** — желательно, если реализуемо (запланировать как «если возможно — да»).
## 2. Компоновка (layout)
- Базовый grid по умолчанию:
- **слева — список задач** (широкая колонка),
- **по центру/право — рабочая область** (в ней живёт «работа бота» / live-шаги);
- рабочая область может **разделяться на сплит-панели** (2×2, не считая левой колонки с задачами).
- Табы пока **не делаем** (первый этап).
- Панели должны быть **сворачиваемыми** (хотя бы левая колонка с задачами).
- Сохранение layout (положение/размеры панелей) — **нужно**, переживает перезапуск приложения (Fyne Preferences).
- Окно: **на весь экран по умолчанию**.
- Нижняя панель: **вкладки «Логи», «Состояние»**:
- **Логи** — ведение логов (поток системных логов);
- **Состояние** — живое состояние текущих задач / на каком статусе сейчас задача.
- Верхняя панель: **меню**, пока только пункт «О программе» (About → About).
- Тема: **тёмная**, без переключателя темы; стартуем с **дефолтной темы Fyne**.
- Локаль: **en** (интерфейс на английском).
## 3. Данные задач
- Задачи отображаются **списком** слева: **краткое название + служебное** (воркшоп).
- Детализация списка (статус/фильтры/поиск/сортировка/по умолчание/пагинация) — **дорабатываем потом**.
- В деталях задачи (рабочая область) показываем все перечисленные поля:
- Title, Goal, Why, AC (acceptance criteria), Repo(s), Tag (task_tag), статус, CreatedAt/UpdatedAt, вложенные трассы (traces), история чата (history), включая (session id opencode).
- История чата из БД (user/assistant) — лента сообщений.
- Трейсы агентов (аналист/dev/reviewer) — дерево шагов/ходов + raw output.
- Live-шаги (LiveRegistry) — восстановить и показывать в реальном времени.
## 4. Действия с задачами
- UI поддерживает все действия, доступные в TG: создание задачи, изменение, **approve**, **rework доработка**, **cancel**, **retry**, закрытие (skip/continue), статусы — в рамках валидных переходов.
- Создание задачи — форма с полями (Title, Goal/Why, Repos, AC) (какие обязательные — уточнить).
- Работа с статусами: эмуляция команд `/approve`, `/retry`, `/cancel` и т.п. (валидные переходы по state machine)
- Подтверждения опасных действий (удаление задачи, и т.п.).
- Редактирование полей (repo, AC, tags) из UI — по необходимости.
## 7. Мониторинг системы
- Показывать **логи** (готовый поток из `log`), **фреймы состояния** бота:
- статус opencode serve/pool, коннекции, активные сессии.
- Live-статус задач (`/status N`) эквивалентом — во вкладке «Состояние».
- Нужен индикатор занятости агента («опенкод думает/dev writing…»).
## 8. Настройки / конфиг
- Настройки в UI пока **не выводим**; секреты никоим образом не показываем.
- Кнопка «Завершить» (выход приложения) — в меню.
- Перезапуск бота из UI — пока не надо.
## 9. Живое обновление / консистентность
- **Шина событий (event-bus)** — запланирована: UI получает события из ядра (новые задачи, изменение статусов, live-шаги, логи). Явно нужен.
- UI и Telegram — **оболочки** вокруг одного ядра; обмен через **интерфейс (абстракцию)**, не зашумляя ядро.
- Консистентность между TG и UI при параллельных изменениях — через события ядра; архитектура обмена — см. раздел 12.
## 10. Технические
- **Fyne**: свежая стабильная (последняя).
- **Абстракция БД**: доступ к storage через **интерфейс** (не прямиком к *storage.Storage) для тестируемости.
- Пакет: **`internal/ui`** (+ подпакеты при необходимости).
- Тесты: юнит-тесты модели, **без e2e/UI-тестов** пока.
- Использование данных: **вся история** (без ограничения по времени).
## 11. Скоуп первого этапа
- Минимальный UI, покрывающий **функционал Telegram-интерфейса** (все команды/действия).
- Также UI — основной интерфейс (TG остаётся каналом).
---
## 12. Архитектура обмена UI ↔ Core (best practices)
### 12.1. Слои и направление зависимостей
```
┌──────────────┐ команды → ┌─────────────────┐ заказы/готовое → ┌──────────────┐
│ UI (Fyne) │ ───────────▶ │ Application / │ ─────────────────▶ │ Domain/core │
│ «представ- │ │ UseCase слой │ события (events) │ (model) │
│ ление» │ ◀─────────── │ │ ◀────────────────── │ │
└──────────────┘ события ← └─────────────────┘ └──────────────┘
```
- **Core** (`internal/core`.ProcessTurn — state-machine) **не трогаем**. Он — источник правды.
- **App** (`internal/app`) — use-case/композиция: политика «одна активная задача на чат», `Notify`, роутинг команд.
- **UI** — ещё одна реализация `chat.Channel` (параллельно Telegram). Уже есть идеальный «port»: интерфейс `Channel{Run, OnMessage, Send, Ask, Close}`.
### 12.2. UI получает состояние, а не управляет Core'ом (uni-directional data flow)
- Core **мутирует состояние** (БД, worker, аналитик). UI — только **читает снимки** и реагирует на события, обновляя своё view-model (не БД и не core).
- Действия UI = **команды** (`CreateTask`, `ApproveTask`, `RetryTask`, `CancelTask`, `ContinueTask`), которые вызывают use-case в Core.
- Это даёт: единый источник правды, простую отладку, лёгкое тестирование без UI.
### 12.3. Обмен = событийная шина (event bus / pub-sub)
- **Core — публикует** доменные события, UI — **подписчик**:
- `TaskCreated{id, chatID}`
- `TaskStatusChanged{id, from, to}` (ready/approved/running/success/failed/timeout…)
- `TaskHistoryAppended{id, role, content}`
- `TraceAppended{id, agent, status, output}`
- `AgentActivity{id, agent, stage}` (live-шаги, индикаторы занятости)
- `LogLine` (системные логи для вкладки «Логи»)
- **Направление однонаправленное**: core/worker/analyst не знают, кто подписан; никаких прямых вызовов Fyne из core!
- Реализация малая: `Bus` с `Subscribe/Unsubscribe/Publish` + buffered channels (`n`-подписчиков или `select`); паттерн уже есть в `chat.Router` (внутренний `incoming` channel).
- Сигнатуры событий использовать **и для консистентности** TG↔UI (оба канала — простые подписчики/клиенты шины).
### 12.4. Модель потоков Fyne (v2.6+) и `fyne.Do`
- Fyne с v2.6.0 выполняет все события/колбэки **на одной главной goroutine**.
- Фоновые goroutine (worker, opencode polling, ход core) **не должны модифицировать UI напрямую**.
- Любое обновление виджетов из своей goroutine — через:
- `fyne.Do(func(){ widget.X = …; widget.Refresh() })` — очередь в следующий кадр;
- `fyne.DoAndWait(...)` — когда надо дождаться завершения;
- унифицировать с **binding** (`binding.String`, `binding.List`) — через `fyne.Do` не требуется, но проще явно.
- Подписчик шины ставит snapshot в очередь UI-обновления и вызывает `fyne.Do`.
### 12.5. Чтение-модель на UI: снапшоты, не мутабельные указатели
- UI получает **копии** записей (Task, history, traces) через абстракцию БД (см. раздел 10).
- Никаких референсов на разделяемые контейнеры core; используются лёгкие view-копии (Value-типы/DT-коды) — нет гонок bottom-up.
### 12.6. Жизненный цикл: Core ≠ UI
- Core (бот) живёт независимо от окна. Закрытие окна = **сворачивание** (Core продолжает).
- Полный выход — только кнопка «Завершить» → `app.Quit()` (Worker.Stop → router.Close → pool.Close → store.Close).
- `--noui` → core запускается без создания окна.
### 12.7. Скоуп реализации (поэтапно)
- **Фаза 1**: `internal/ui` со сплитами + `TaskStore`-абстракция + **односторонний** поток: команды → Core, периодические snapshots из БД (без шины), всё через `fyne.Do`.
- **Фаза 2**: добавить `event.Bus` — Core публикует события, UI подписывается (статусы, история, live-шаги).
- **Фаза 3**: консистентность TG↔UI через общий маршрутизатор/шину (`UserID`, `chat.Router`).
---
## Открытые пункты (TODO)
- [ ] Точно определить set сплит-панелей (2x2 центр) и как добавляются
- [ ] Обязательные поля формы создания задачи
- [ ] Подробности виджета списка задач (после первого мильстон)
- [x] Архитектура обмена UI<->Core (раздел 12 — принципы; детальные контракты событий/интерфейсов — следующая итерация)