Files
ratatoskr-go/docs/ui-spec.md
ki.sagidullin 8ba1a8aa00
Some checks failed
CI / test (pull_request) Failing after 1m17s
CI / build-and-package (amd64, linux) (pull_request) Failing after 1m2s
CI / build-and-package (amd64, windows) (pull_request) Successful in 28s
refactor(ui): вынести панель «Состояние» (StatePanel) из монолита окна
Спеца 12.13: интерфейс + Nil + чистые хелперы (StateText/TraceLine/
ActivityLine/TaskStatusText/StatusBadge/Itoa) в internal/ui/state_panel.go,
Fyne-реализация в internal/ui/desktop/state_panel.go. window.go — чистая
компоновка: все 5 панелей вынесены как контракты.
2026-08-20 23:54:27 +05:00

20 KiB
Raw Blame History

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 — Store-абстракция (копии), окно как chat.Channel, команды через канал, snapshots из БД. Fyne-реализация — internal/ui/desktop (build-tag cgo, требует компилятор 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; строка роли = pure FormatRole(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 — принципы; детальные контракты событий/интерфейсов — следующая итерация)