Спеца 12.13: интерфейс + Nil + чистые хелперы (StateText/TraceLine/ ActivityLine/TaskStatusText/StatusBadge/Itoa) в internal/ui/state_panel.go, Fyne-реализация в internal/ui/desktop/state_panel.go. window.go — чистая компоновка: все 5 панелей вынесены как контракты.
20 KiB
UI-спека Ratatoskr (Fyne Desktop)
Статус: утверждается. Черновик, обсуждаем дальше (архитектура обмена UI <-> Core).
Стек: Go + Fyne (свежая стабильная версия). Платформа: Windows (Linux пока не поддерживаем).
1. Цель и роль UI
- UI — полноценный инструмент (не только админ-панель): управляет всем, чем управляет Telegram, и становится основным интерфейсом.
- Один пользователь, один человек (без multi-user / мультисессии).
- UI должен уметь всё, что умеет Telegram-интерфейс, и станет основным; Telegram остаётся как один из каналов.
- UI — графическая оболочка самого приложения (одна программа), не отдельный process.
- Внутри оболочки запускается ботик сам; UI — это лишь ещё одна реализация
chat.Channel. - Запуск: обычный запуск бинаря → UI. Режим без UI — только по флагу
--noui.- Логика: UI включён по умолчанию, headless — опционально (
--noui).
- Логика: UI включён по умолчанию, headless — опционально (
- Трей-иконка не нужна.
- При закрытии окна — сворачивать (не завершать процесс). Для полного выхода — отдельная кнопка «Завершить», чтобы бот остановился.
- Конфиг-секции редактировать в 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(внутреннийincomingchannel). - Сигнатуры событий использовать и для консистентности 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— Store-абстракция (копии), окно какchat.Channel, команды через канал, snapshots из БД. Fyne-реализация —internal/ui/desktop(build-tagcgo, требует компилятор C для GLFW). - Фаза 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.
12.11. Контракт виджета «Детали задачи» (TaskDetailPanel)
Детали выбранной задачи — верх right-сплита: заголовок, статус, детали
(цель + репозитории).
ShowTask(t)— рендер из снимкаstorage.Task: заголовок (bold), статус (italic), детали =TaskDetailText(t).ShowEmpty()— сброс к placeholder («—» / пусто) при недоступной задаче.- Строка деталей = pure
TaskDetailText(t)(цель + репо), тестируется без Fyne. - Обновление — на потоке Fyne; вызывающий уже внутри
fyne.Do. - Диалог и «Состояние» — отдельные панели (chat_panel, state_panel), здесь не участвуют; окно оркестрирует загрузку всех панелей по выбору задачи.
12.12. Контракт виджета «Диалог» (ChatPanel)
Диалог — транскрипт общения с выбранной задачей + композитор (поле ввода и кнопки команд).
- Транскрипт:
SetTranscript(text)— полный рендер истории выбранной задачи;Append(text)— добавить строку; при превышении лимита старые строки отбрасываются (буфер ограничен);Clear()— очистить при недоступной задаче.
- Композитор: поле ввода + кнопки команд, подключённые к
ui.CommandsчерезSetCommands(c)(ввод →SendText, кнопки → Start/Approve/Skip/Cancel). - Доставка: окно (как chat.Channel) рендерит
Send/Ask/историю черезAppend; строка роли = pureFormatRole(role)(👤/🤖). - Обновление — на потоке Fyne; вызывающий уже внутри
fyne.Do.
12.13. Контракт виджета «Состояние» (StatePanel)
Панель «Состояние» — агенты + трейсы выбранной задачи: снимок и live-строки.
SetText(text)— полный рендер снимка трасс выбранной задачи.Append(text)— добавить строку; при превышении лимита старые строки отбрасываются (буфер ограничен).- Строка снимка = pure
StateText(traces)(агент: статус, session). - Live-строки (чистые хелперы, окно зовёт
Appendс готовой строкой):TraceLine(agent, status)— добавлен/обновлён трейс субагента;ActivityLine(agent, stage)— смена этапа агента;TaskStatusText(e)— изменение статуса задачи (бейдж + ID).
- Обновление — на потоке Fyne; вызывающий уже внутри
fyne.Do.
Открытые пункты (TODO)
- Точно определить set сплит-панелей (2x2 центр) и как добавляются
- Обязательные поля формы создания задачи
- Подробности виджета списка задач (после первого мильстон)
- Архитектура обмена UI<->Core (раздел 12 — принципы; детальные контракты событий/интерфейсов — следующая итерация)