Спеца 12.10: интерфейс + Nil + TaskTitle в internal/ui/task_list_panel.go, Fyne-реализация в internal/ui/desktop/task_list_panel.go. Состояние выбора живёт в окне; выбранная строка подсвечивается (Select/Unselect).
201 lines
17 KiB
Markdown
201 lines
17 KiB
Markdown
# 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. Скоуп реализации (поэтапно)
|
||
|
||
- [x] **Фаза 1**: `internal/ui` — Store-абстракция (копии), окно как `chat.Channel`,
|
||
команды через канал, snapshots из БД. Fyne-реализация — `internal/ui/desktop`
|
||
(build-tag `cgo`, требует компилятор C для GLFW).
|
||
- [x] **Фаза 2**: `internal/events` — Bus/Hub, Core публикует события, UI подписывается
|
||
через `Controller` (статусы, история, live-шаги, логи).
|
||
- [ ] **Фаза 3**: консистентность TG↔UI через общий маршрутизатор/шину (`UserID`, `chat.Router`).
|
||
|
||
Примечание: окно (Фаза 1) собирается только с cgo; на машинах без C-компилятора
|
||
приложение работает headless (UI=nil). Реальное окно полноценно проверяется
|
||
на машине с MinGW-w64/gcc (`go build -ldflags ...`), здесь — только typecheck.
|
||
|
||
### 12.8. Структура UI-кода: компоненты-виджеты (рефакторинг)
|
||
|
||
`internal/ui` (без Fyne) — контракты + Nil-реализации + юнит-тесты. Fyne-код —
|
||
только в `internal/ui/desktop/*`, один файл — одна панель. `desktop/window.go`
|
||
становится чистой компоновкой.
|
||
|
||
Каждый компонент:
|
||
- **интерфейс** в `internal/ui/<panel>.go` (контракт для тестов/headless);
|
||
- **Nil-реализация** в том же файле (headless/тесты);
|
||
- **Fyne-реализация** в `internal/ui/desktop/<panel>.go`.
|
||
|
||
### 12.9. Контракт виджета «Логи» (LogPanel)
|
||
|
||
Панель «Логи» — поток системных логов (сырые строки из `events.LogLine`).
|
||
|
||
- `Append(text)` — добавить строку лога; при превышении лимита старые строки
|
||
отбрасываются (буфер ограничен, не расти бесконечно).
|
||
- Контейнер виджета — отдаётся окну для встраивания в таб «Логи» (Scroll).
|
||
- Обновление содержимого — на потоке Fyne (внутри панели `fyne.Do`), не из
|
||
горутины Hub.
|
||
|
||
### 12.10. Контракт виджета «Список задач» (TaskListPanel)
|
||
|
||
Список задач — левая панель окна: все задачи (`storage.Task`) строками.
|
||
|
||
- `SetTasks(tasks)` — заменить снимок списка и перерисовать виджет. Данные —
|
||
копии из Store (спец 12.5).
|
||
- `OnSelect(fn)` — колбэк клика по строке; окно грузит детали выбранной задачи
|
||
(это `task_detail`, отдельный компонент). Состояние выбора живёт в окне.
|
||
- Выбранная строка подсвечивается (`Select`/`Unselect`), окно управляет ею.
|
||
- Строка = `taskTitle(t)` (pure-функция, тестируется без Fyne).
|
||
- Обновление — на потоке Fyne; вызывающий уже внутри `fyne.Do`, панель сама
|
||
виджет не трогает из горутин Hub.
|
||
|
||
---
|
||
|
||
## Открытые пункты (TODO)
|
||
|
||
- [ ] Точно определить set сплит-панелей (2x2 центр) и как добавляются
|
||
- [ ] Обязательные поля формы создания задачи
|
||
- [ ] Подробности виджета списка задач (после первого мильстон)
|
||
- [x] Архитектура обмена UI<->Core (раздел 12 — принципы; детальные контракты событий/интерфейсов — следующая итерация) |