diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md new file mode 100644 index 00000000..bc179dfc --- /dev/null +++ b/ARCHITECTURE.ru.md @@ -0,0 +1,286 @@ +# Архитектура Kanban Code + +Перевод и адаптация [`docs/architecture.md`](docs/architecture.md) и [`CLAUDE.md`](CLAUDE.md) для русскоязычных контрибьюторов. + +Документ объясняет, как устроено ядро приложения, почему выбран Elm-подобный однонаправленный поток данных, и какие подводные камни Swift 6 надо знать, чтобы не получить креш на ровном месте. + +--- + +## Зачем такая архитектура + +До переписывания приложение имело **два источника правды**: in-memory `BoardState.cards` и `links.json` на диске через `CoordinationStore`. На состояние писали **5 независимых писателей** параллельно. Симптомы: + +- карточки прыгали между колонками, +- терминалы исчезали, +- при быстром создании появлялись дубли. + +Каждая заплатка порождала новые edge cases. Решение — лёгкий стор в духе Elm/Redux: все мутации состояния идут через одну чистую функцию-редьюсер. **Это не TCA** (The Composable Architecture от Point-Free), а ~400 строк своего кода с теми же базовыми гарантиями. + +--- + +## Основные компоненты + +### `AppState` (struct) + +Единственный источник правды. Все данные доски живут здесь. + +``` +AppState +├── links: [String: Link] // cardId → Link (карточки) +├── sessions: [String: Session] // sessionId → Session +├── activityMap: [String: ActivityState] // sessionId → активность +├── tmuxSessions: Set // активные tmux-сессии +├── selectedCardId: String? +├── selectedProjectPath: String? +├── configuredProjects: [Project] +├── error: String? +└── computed: cards, filteredCards, visibleColumns +``` + +### `Action` (enum) + +Исчерпывающий список того, что может произойти. Любое изменение состояния начинается с диспатча action-а. + +- **UI-действия:** `createManualTask`, `createTerminal`, `launchCard`, `resumeCard`, `moveCard`, `renameCard`, `archiveCard`, `deleteCard`, `selectCard`, `unlinkFromCard`, `killTerminal`, `addBranchToCard`, `addIssueLinkToCard`, `addExtraTerminal` +- **Завершение асинхронных операций:** `launchCompleted`, `launchFailed`, `resumeCompleted`, `resumeFailed`, `terminalCreated`, `terminalFailed` +- **Фоновые:** `reconciled` (один атомарный апдейт после скана) +- **Настройки:** `setError`, `setSelectedProject`, `setLoading` + +### `Reducer` (чистая функция) + +Сигнатура: `(inout AppState, Action) -> [Effect]` + +- Синхронная. Без `async`. Без сайд-эффектов. +- Полностью тестируется: дай состояние и action → проверь новое состояние и список эффектов. +- Работает на `@MainActor` (тот же поток, что и UI) — никаких гонок между мутациями. + +### `Effect` (enum) + `EffectHandler` (actor) + +Сайд-эффекты, объявленные редьюсером, исполняются асинхронно через `EffectHandler`: + +- `persistLinks`, `upsertLink`, `removeLink` — дисковый I/O +- `createTmuxSession`, `killTmuxSession` — управление терминалом +- `deleteSessionFile`, `cleanupTerminalCache` — очистка +- `updateSessionIndex` — метаданные сессий + +### `BoardStore` (`@Observable @MainActor`) + +Главный стор, который связывает всё вместе: + +```swift +func dispatch(_ action: Action) { + let effects = Reducer.reduce(state: &state, action: action) + for effect in effects { + Task { await effectHandler.execute(effect, dispatch: dispatch) } + } +} +``` + +Также есть `reconcile()` — асинхронный метод, который делает полный discovery (сессии, tmux, worktree, PR) и диспатчит `.reconciled(result)`. + +--- + +## Ключевые файлы + +| Файл | Роль | +|------|------| +| `Sources/KanbanCodeCore/UseCases/BoardStore.swift` | AppState, Action, Reducer, BoardStore | +| `Sources/KanbanCodeCore/UseCases/EffectHandler.swift` | Асинхронное исполнение эффектов | +| `Sources/KanbanCodeCore/Domain/Entities/Link.swift` | Сущность карточки (есть `isLaunching: Bool?`) | +| `Sources/KanbanCode/ContentView.swift` | Главная вьюха — диспатчит action-ы, запускает async-флоу launch/resume | +| `Sources/KanbanCode/BoardView.swift` | Колонки доски — читает из `store.state`, диспатчит move/rename/archive | +| `Sources/KanbanCode/CardDetailView.swift` | Панель карточки — читает данные, диспатчит через колбэки | +| `Sources/KanbanCodeCore/UseCases/BackgroundOrchestrator.swift` | Только уведомления и поллинг активности (колонки больше не трогает) | +| `Tests/KanbanCodeCoreTests/ReducerTests.swift` | Чистые тесты редьюсера | + +--- + +## Поток данных + +``` +Действие пользователя / таймер / hook-событие + │ + ▼ + store.dispatch(.action) + │ + ▼ + Reducer.reduce(state, action) ← чистая, синхронная, @MainActor + │ │ + ▼ ▼ + state замутирован [Effect]-ы возвращены + │ + ▼ + EffectHandler.execute() ← async, actor-isolated + │ + ▼ + disk / tmux / cleanup + │ + ▼ (если нужен completion-action) + store.dispatch(.completed) +``` + +--- + +## Защита от гонок + +### Флаг `isLaunching` + +Когда карточка запускается или резюмится, редьюсер ставит `isLaunching = true`. Фоновая реконсиляция (`.reconciled`) **пропускает** любую карточку с `isLaunching == true` — это не даёт карточке прыгать между колонками, пока асинхронная работа ещё не закончилась. + +``` +dispatch(.resumeCard) → column = .inProgress, isLaunching = true +dispatch(.reconciled) → ПРОПУСКАЕТ эту карточку (isLaunching защищает) +dispatch(.resumeCompleted) → isLaunching = nil, карточка остаётся в .inProgress +``` + +### Именование терминалов + +Терминалы используют имя `"card-{id.prefix(12)}"` вместо имени проекта — это предотвращает коллизии между карточками одного проекта. + +### `createTerminal` не меняет колонку + +Редьюсер на `.createTerminal` ставит `tmuxLink` с `isShellOnly: true`, но **не** меняет колонку. Shell-терминал — это не работающий Claude, карточка остаётся там, где была. + +--- + +## Чем это отличается от TCA + +Это **не** TCA (The Composable Architecture от Point-Free). Ключевые различия: + +| Фича | Наш стор | TCA | +|------|----------|-----| +| Dependency injection | Параметры init | Система `@Dependency` | +| Скоупинг стора | Передаём `store.state` напрямую | `Store.scope()` + `ViewStore` | +| Отмена эффектов | Обычные `Task` | Сложный лайфсайкл эффектов | +| Состояние навигации | `@State` во вьюхах | Управляется в редьюсере | +| Зависимости пакетов | Нет | `swift-composable-architecture` | +| Размер кода | ~400 строк | Целый фреймворк | + +Для нашего кейса (одноэкранное приложение, ~25 action-ов, главная проблема — гонки) лёгкий подход даёт те же гарантии без кривой обучения TCA. + +--- + +## Тестирование редьюсера + +Тесты редьюсера — чистые и быстрые: ни диска, ни async, ни моков. + +```swift +@Test func resumeCardNoBounce() { + var state = stateWith([waitingCard]) + + // Пользователь резюмит карточку + Reducer.reduce(state: &state, action: .resumeCard(cardId: "card1")) + #expect(state.links["card1"]?.column == .inProgress) + #expect(state.links["card1"]?.isLaunching == true) + + // Фоновая реконсиляция срабатывает — НЕ должна перебить + Reducer.reduce(state: &state, action: .reconciled(result)) + #expect(state.links["card1"]?.column == .inProgress) // всё ещё защищено +} +``` + +Запуск всех тестов: + +```bash +swift test +``` + +--- + +## Критично: DispatchSource + @MainActor → креш + +SwiftUI-вьюхи имеют изоляцию `@MainActor`. В Swift 6 замыкания, созданные внутри `@MainActor`-методов, **наследуют** эту изоляцию. Если обработчик события `DispatchSource` выполняется на фоновой GCD-очереди, runtime срабатывает ассерт и **крешит** приложение (`EXC_BREAKPOINT` в `_dispatch_assert_queue_fail`). + +**Так делать НЕЛЬЗЯ** (крешится в рантайме, без warning-а на компиляции): + +```swift +// Внутри SwiftUI View (она @MainActor) +func startWatcher() { + let source = DispatchSource.makeFileSystemObjectSource( + fd: fd, eventMask: .write, queue: .global()) + source.setEventHandler { + // КРЕШ: это замыкание наследует @MainActor, + // но исполняется на фоновой очереди + NotificationCenter.default.post(name: .myEvent, object: nil) + } +} +``` + +**Правильно** — вынести в `nonisolated`-контекст: + +```swift +// Вариант A: nonisolated static-фабрика +private nonisolated static func makeSource(fd: Int32) -> DispatchSourceFileSystemObject { + let source = DispatchSource.makeFileSystemObjectSource( + fd: fd, eventMask: .write, queue: .global()) + source.setEventHandler { + NotificationCenter.default.post(name: .myEvent, object: nil) + } + source.resume() + return source +} + +// Вариант B: nonisolated async-функция с AsyncStream +private nonisolated func watchFile(path: String) async { + let source = DispatchSource.makeFileSystemObjectSource(...) + let events = AsyncStream { continuation in + source.setEventHandler { continuation.yield() } + source.setCancelHandler { continuation.finish() } + source.resume() + } + for await _ in events { + NotificationCenter.default.post(name: .myEvent, object: nil) + } +} +``` + +Правило применимо к **любому** GCD-колбэку (`setEventHandler`, `setCancelHandler`, `DispatchQueue.global().async`), вызванному из `@MainActor`-контекста. + +--- + +## Тулбар (macOS 26 Liquid Glass) + +Тулбар использует SwiftUI `.toolbar` с `ToolbarSpacer` (macOS 26+) для отдельных glass-пилюль: + +- **`.navigation`** = левая сторона. Все элементы сливаются в **одну** пилюлю (спейсеры не помогают). +- **`.principal`** = центр. Отдельная пилюля от navigation. +- **`.primaryAction`** = правая сторона. `ToolbarSpacer(.fixed)` **создаёт** отдельные пилюли здесь. +- Для элементов внутри `.navigation`, которым нужна своя пилюля, используй `Menu` (не `Text`): menu маппится в `NSPopUpButton`, который получает отдельный glass автоматически. + +--- + +## Правила работы со стором (не нарушать) + +1. **Никогда** не мутируй `AppState` напрямую из вьюх. +2. **Никогда** не пиши в `CoordinationStore` из вьюх — всегда диспатч action-а. +3. Любой новый сайд-эффект — это новый `Effect`-кейс + ветка в `EffectHandler`. Не запускай `Task` прямо во вьюхе. +4. Если карточка может оказаться в полусломанном состоянии во время асинхронной операции — поставь `isLaunching = true` в редьюсере и не забудь снять в `.completed`/`.failed`. + +--- + +## Конвенциональные коммиты + +Используй [Conventional Commits](https://www.conventionalcommits.org/). Release-please на их основе автоматически генерит CHANGELOG. + +- `feat: add dark mode` — новая фича (минорный bump) +- `fix: correct session dedup` — баг-фикс (патч) +- `perf: speed up branch discovery` — производительность (патч) +- `refactor: extract hook manager` — рефакторинг (скрыто из changelog) +- `docs: update README` — документация (скрыто) +- `chore: bump deps` — обслуживание (скрыто) +- `feat!: redesign board layout` — breaking change (мажорный bump) + +--- + +## Где искать креши и логи + +- macOS crash reports: `~/Library/Logs/DiagnosticReports/KanbanCode-*.ips` +- Логи приложения: `~/.kanban-code/logs/kanban-code.log` +- Настройки пользователя: `~/.kanban-code/settings.json` +- Карточки и связи: `~/.kanban-code/links.json` + +--- + +## Легаси: `BoardState.swift` + +`BoardState.swift` оставлен как мёртвый код — UI его не использует. `BoardStateIntegrationTests` всё ещё гоняют его в регрессионных целях. Можно удалить в следующем cleanup-проходе. diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 00000000..a4b9fc2e --- /dev/null +++ b/README.ru.md @@ -0,0 +1,239 @@ +# Kanban Code — краткая инструкция (по-русски) + +Нативное приложение-канбан для управления сессиями Claude Code: карточки, tmux-сессии, git-worktree и PR в одном окне. + +Этот файл кратко объясняет, как установить, запустить и протестировать проект, содержит примеры команд CLI и быстрый сценарий демонстрации. + +Кому это нужно: разработчикам, которые хотят запускать несколько сессий Claude Code параллельно и контролировать каждую задачу как карточку на доске. + +Коротко: карточка = сессия Claude + (опционально) worktree + tmux + PR/issue. Приложение автоматизирует создание worktree, управление tmux и отслеживание PR. + +--- + +## Для чайников — пошаговая инструкция (очень просто) + +1) Откройте терминал и перейдите в папку проекта: + +```bash +cd ~/Projects/Canban/kanban-code +``` + +2) Проверьте, что у вас установлен Swift (macOS) и Node (для CLI): + +```bash +swift --version # macOS: должен быть Swift 6+ +node --version # Node.js для CLI (если планируете использовать CLI) +``` + +3) Запуск приложения на macOS (самый простой способ для начинающих): + +- Если хотите просто запустить приложение из исходников, выполните: + +```bash +make run-app +``` + +Если при запуске вы видите ошибку `make: *** No rule to make target 'run-app'`, значит вы не в той папке: сначала выполните `cd kanban-code` (см. шаг 1). + +4) Запуск CLI локально (альтернатива, если GUI не нужен): + +```bash +cd cli +npm install +node ./dist/kanban.js list # или: npx node ./dist/kanban.js list +``` + +Если хотите установить удобную команду `kanban` в `~/.local/bin`, вернитесь в корень (`cd ..`) и выполните: + +```bash +make install-cli +``` + +5) Частые проблемы и их простое решение: + +- macOS блокирует запуск приложения: правый клик по приложению → "Open", затем System Settings → Privacy & Security → "Open Anyway". +- Ошибка отсутствия `gh` (GitHub CLI): установите через Homebrew `brew install gh`. +- Ошибка кодовой подписи (codesign): для локальной разработки обычно можно проигнорировать, если приложение запускается напрямую. + +6) Если нужно — сохраните вывод команд в файл и пришлите мне его, я помогу с разбором: + +```bash +make run-app 2>&1 | tee /tmp/kanban_run_output.txt +``` + + +## Быстрый старт (демо за 5 минут) + +Склонируйте репозиторий (у вас уже склонировано): + +```bash +git clone https://github.com/langwatch/kanban-code.git +cd kanban-code +``` + +Запустить macOS-версию (на macOS 26, Swift 6): + +```bash +make run-app +``` + +Для Windows (Tauri): + +```bash +cd windows +npm install +npm run tauri dev +``` + +CLI (`kanban`) можно установить локально или использовать встроенный: + +```bash +# установить в ~/.local/bin +make install-cli + +# пример команд +kanban list +kanban show +kanban capture +kanban send "hello" +``` + +--- + +## Установка и зависимости + +- macOS: требуется Swift (в комплекте в Xcode/Swift toolchain), `tmux` (рекомендуется), опционально `gh` (GitHub CLI), `mutagen` для удалённого исполнения, Pushover для уведомлений. +- Windows: Node.js (v18+), Rust (для Tauri), опционально `gh`. + +Приложение работает без большинства опциональных инструментов, но функциональность PR/issue/remote зависит от соответствующих утилит. + +--- + +## Как тестировать локально + +1. Убедитесь, что Swift установлен: + +```bash +swift --version +``` + +2. Запустите тесты (модульные): + +```bash +swift test +``` + +Примечание: интеграционные тесты требуют внешних инструментов/сессий (Claude/Gemini/gh), они могут быть помечены как пропущенные. + +Если `swift test` долго собирает или пропускает тесты, сохраните вывод в файл для анализа: + +```bash +swift test | tee /tmp/kanban_test_output.txt +``` + +--- + +## CLI — быстрые примеры + +- Посмотреть все карточки (группировка по колонкам): + +```bash +kanban list +``` + +- Показать карточку подробно: + +```bash +kanban show card_2MtCMwX +``` + +- Отправить сообщение в сессию карточки (tmux): + +```bash +kanban send card_2MtCMwX "Продолжай, пожалуйста" +``` + +- Захватить последние строки терминала карточки: + +```bash +kanban capture card_2MtCMwX +``` + +Все команды поддерживают флаг `--json` для машинной обработки. + +--- + +## Структура проекта (коротко) + +- `Sources/` — Swift-код macOS-приложения и библиотек `KanbanCode`, `KanbanCodeCore`. +- `cli/` — Node.js/TypeScript реализация CLI (`kanban`). +- `windows/` — Tauri + React фронтенд для Windows. +- `Tests/` — тесты Swift для ядра. +- `spec/` — спецификации функций и сценариев. + +--- + +## Архитектура (essentials) + +- Чистая архитектура (port & adapter). Корневой поток: Action → Reducer → EffectHandler. +- В `KanbanCodeCore` лежат сущности (Session, Link, Worktree, PullRequest) и интерфейсы для адаптеров (Git, Tmux, Claude CLI). +- UI реагирует на одно централизованное состояние `AppState`. + +--- + +## Быстрая демонстрация (сценарий для доклада) + +1. Открыть приложение / запустить `make run-app`. +2. В `All Sessions` должна автоматически появиться обнаруженная сессия (если есть `~/.claude/projects`). +3. Выбрать карточку, открыть терминал внутри карточки и показать прикладную команду (например, запустить тесты в worktree). +4. Показать автоматическое перемещение карточки: Claude отправил PR → карточка в `In Review`. +5. Показать очистку worktree после слияния — карточка переходит в `Done`. + +Для доклада можно подготовить заранее скриншоты из `assets/` (`assets/screenshot.webp`, `assets/productive-mode.webp`) или запустить небольшой live-demo с одной локальной Claude-сессией. + +--- + +## Настройки (файл пользователя) + +По умолчанию настройки хранятся в `~/.kanban-code/settings.json`. + +Пример конфига: + +```json +{ + "projects": [ + {"path": "/Users/you/Projects/my-app", "github": {"issueFilters": "assignee:@me state:open"}} + ], + "pushover": {"userKey": "...", "apiToken": "..."} +} +``` + +Карточки и связи хранятся в `~/.kanban-code/links.json`. + +--- + +## Полезные команды разработки + +- Запустить линтер/форматтер (если есть в CI): смотреть `Makefile` и `cli/package.json`. +- Собрать Windows dev: `cd windows && npm install && npm run tauri dev`. +- Установить локально CLI: `make install-cli`. + +--- + +## Частые проблемы и отладка + +- Если macOS блокирует приложение при первом запуске — правый клик → Open, затем System Settings → Privacy & Security → Open Anyway. +- Если отсутствуют сессии — убедитесь, что `~/.claude/projects/` содержит сессии или запустите Claude Code CLI и создайте одну. +- Интеграция с GitHub требует `gh` и токена, убедитесь, что `gh auth status` показывает авторизацию. + +--- + +## Лицензия и вклад + +Проект распространяется под AGPLv3. Файлы лицензии находятся в `LICENSE`. + +Если нужно — могу перевести остальные документы (CONTRIBUTING, SPEC) и подготовить слайды на русском для презентации. + +--- + +Автор: LangWatch (оригинальный репозиторий)