- Added core documentation for the Team Management feature, including iteration goals, implementation plans, and design specifications. - Introduced new files for team configuration, IPC channels, and UI components for team listing and Kanban board. - Established a clear definition of done and scope for the first iteration, focusing on visible results and graceful degradation in the UI. - Documented the messaging and task file formats, ensuring robust communication between team members. - Implemented a Kanban design with defined states and transitions for task management.
18 KiB
Итерация 01 — Core Foundation + Team List
Эта итерация реализует первый вертикальный срез Team Management: от файлов ~/.claude/teams/* до UI, где открывается вкладка со списком команд.
База для решений и порядка работ:
docs/team-management/implementation.md(v7)~/.claude/plans/goofy-napping-river.md(summary v7)docs/team-management/research-*.md+docs/team-management/kanban-design.md
Цель итерации
Сделать видимый результат без “backend-first” провала:
- В приложении есть вкладка
Teams(тип табаteams, singleton в рамках активной pane, какNotifications) - В ней отображается список команд из
~/.claude/teams/{teamName}/config.json - Если команд нет (или директория отсутствует) — корректный empty state
- Ошибки чтения/парсинга не валят UI: graceful degradation
Не-цели (строго вне scope)
- Team detail (страница конкретной команды)
- Members/Tasks/Messages/Kanban UI
- Любые write-path (inbox write, kanban update, task status update)
- FileWatcher расширение (teamsWatcher/tasksWatcher) и live refresh по
team-change - Tests/fixtures (это отдельная итерация 5), кроме минимальных правок тестов, если они ломаются из‑за добавления нового
tab.type
Контракт итерации (что именно считаем “готово”)
API-контракт (Main → Preload → Renderer)
IPC channel
TEAM_LIST='team:list'(flatexport const, безIPC_CHANNELS.*)
Response
- Возвращает
TeamSummary[] - Никаких side effects (только чтение)
- Должен быть безопасен при отсутствии
~/.claude/teams
TeamSummary (на итерации 1)
name: string— отображаемое имя команды (изconfig.json)description: string— описание (изconfig.jsonили'')memberCount: number—config.members.lengthили 0taskCount: number— 0 (пока не читаем tasks)lastActivity: string | null— null (пока не читаем inbox)
Definition of Done
- UI: вкладка
Teamsоткрывается, показывает список/empty state, без падений при:- отсутствии
~/.claude/teams - наличии “мусорных” директорий без
config.json(напримерdefault/) - наличии битого
config.json(пропускаем, не падаем)
- отсутствии
- IPC: есть канал
TEAM_LIST('team:list'), который возвращаетTeamSummary[] - Типы:
IpcResult<T>дедуплицирован вsrc/shared/types/ipc.ts - Качество: проходит
pnpm typecheck, приложение запускается в dev, вкладка доступна из UI
Главные решения, которые сохраняем
- Vertical slice: делаем end-to-end минимум (types → main → IPC → preload → renderer → UI)
- Flat IPC constants: только
export const TEAM_LIST = 'team:list'и прямые импорты (никакихIPC_CHANNELS.*) - Graceful skip:
listTeams()пропускает директории без валидногоconfig.json(v7 fix #37) - Global TeamDataService: сервис создаётся глобально (по аналогии с UpdaterService), не привязан к ServiceContext (подготовка к следующим итерациям, v6/v7 решение)
Выходные изменения (файлы), только для итерации 1
Цель — чтобы по итогам итерации было понятно: что добавили, и что менять не пришлось.
Новые файлы (ожидаемо)
src/shared/types/ipc.tssrc/shared/types/team.ts(минимальный набор типов для list)src/main/services/team/TeamConfigReader.tssrc/main/services/team/TeamDataService.ts(минимумlistTeams(); остальное — позже)src/main/ipc/teams.ts(минимум:TEAM_LIST)src/renderer/utils/unwrapIpc.tssrc/renderer/store/slices/teamSlice.ts(минимум:fetchTeams)src/renderer/components/team/TeamListView.tsxsrc/renderer/components/team/TeamEmptyState.tsx
Изменяемые файлы (ожидаемо)
src/shared/types/index.ts(реэкспорты)src/preload/constants/ipcChannels.ts(добавитьTEAM_LIST)src/shared/types/api.ts(добавитьTeamsAPI.list)src/preload/index.ts(пробросapi.teams.list)src/main/ipc/handlers.ts(wiring: init + registerTEAM_LIST)src/main/index.ts(создание globalteamDataServiceи проброс вinitializeIpcHandlers)src/renderer/types/tabs.ts(добавитьtype: 'teams')src/renderer/store/types.ts,src/renderer/store/index.ts(подключение teamSlice)- layout-компоненты табов (
PaneContent,TabBar,SortableTab) — чтобы появился вход в Teams tab
Порядок работ (runbook)
Ниже — “как делаем” в точном порядке, чтобы на каждом шаге был компилируемый код и минимальный риск сломать существующие фичи.
Контрольные точки (чтобы не тащить поломки дальше)
Контрольные точки привязаны к реально “широким” изменениям (типы/IPC/таб‑routing):
- CP0: после 1.1 —
pnpm typecheck - CP1: после 2.5 —
pnpm typecheck - CP2: после 4.2 —
pnpm typecheck - CP3: после 4.4 —
pnpm dev+ ручная проверка
1) Shared foundation (Phase 0)
1.1 Дедупликация IpcResult<T> (Step 0.1)
- Создать
src/shared/types/ipc.tsсexport interface IpcResult<T = void> { success; data?; error? } - Удалить дубликаты:
src/main/ipc/config.ts: заменитьConfigResult<T>→IpcResult<T>из@shared/typessrc/preload/index.ts: заменить локальныйIpcResult→ импорт из@shared/typesи обновитьinvokeIpcWithResult<T>()на этот тип
- Обновить
src/shared/types/index.ts: реэкспортIpcResult
CP0: pnpm typecheck (на этом шаге обязателен, чтобы не тащить дальше ошибки типов).
2) Backend: чтение команд (Main Process)
2.1 Типы для Teams (Step 1 — частично)
В этой итерации нужны минимум:
TeamConfig(чтобы распарситьconfig.json)TeamSummary(чтобы вернуть данные в UI)
Создать/расширить src/shared/types/team.ts и реэкспорт в src/shared/types/index.ts.
Важно:
TeamSummary.lastActivityна итерации 1 можно держатьnull(не считаем активность без inbox/tasks).
2.2 Path helpers (Step 2 — по минимуму)
Добавить getTeamsBasePath() в src/main/utils/pathDecoder.ts рядом с getProjectsBasePath() / getTodosBasePath():
getTeamsBasePath(): string→path.join(getClaudeBasePath(), 'teams')
Эта функция обязательна уже в итерации 1, чтобы не раскидывать 'teams' как литерал по коду.
Важно (без двусмысленностей): TeamConfigReader не имеет права “захардкодить” путь в конструкторе так, чтобы он перестал учитывать setClaudeBasePathOverride().
getClaudeBasePath()в этом репо поддерживает override (через настройки Claude root).- Поэтому путь к
~/.claude/teamsдолжен вычисляться на каждый вызовlistTeams()черезgetTeamsBasePath().
2.3 TeamConfigReader.listTeams() (Step 3 — частично)
Создать src/main/services/team/TeamConfigReader.ts с поведением:
- Если
~/.claude/teamsотсутствует → вернуть[] readdir(teamsDir, { withFileTypes: true })→ обрабатывать только директории (Dirent.isDirectory), остальные entries игнорировать- Для каждой директории:
- пытаться прочитать
{dir}/config.json - если файла нет / JSON битый / схема не подходит → continue (v7 fix #37)
- собрать
TeamSummary(name, description, memberCount, taskCount=0, lastActivity=null)
- пытаться прочитать
Нюанс: в логах можно оставлять debug (но не спамить и не падать).
2.4 TeamDataService.listTeams() (Step 5 — частично)
Создать каркас TeamDataService так, чтобы в итерации 1 он поддерживал минимум:
listTeams(): Promise<TeamSummary[]>→ делегирует вTeamConfigReader
Остальные методы (getTeamData, kanban/inbox/tasks) — не трогаем или оставляем на следующую итерацию, чтобы не расползаться.
2.5 Глобальная инициализация сервиса (Step 11 — частично)
В src/main/index.ts создать один instance teamDataService (global), не в ServiceContext.
Цель итерации 1: чтобы IPC handler мог вызвать teamDataService.listTeams() без привязки к workspace.
CP1: pnpm typecheck (до IPC лучше убедиться, что main компилируется).
3) IPC + Preload: TeamsAPI.list()
3.1 IPC channels (Step 6)
В src/preload/constants/ipcChannels.ts добавить:
export const TEAM_LIST = 'team:list';
3.2 IPC handler (Step 7 — частично)
Создать src/main/ipc/teams.ts в стиле проекта:
- module-level state +
initializeTeamHandlers(service) registerTeamHandlers(ipcMain)регистрируетTEAM_LISTwrapTeamHandler()возвращаетIpcResult<T>handleListTeams()вызываетservice.listTeams()
Важно:
- Использовать flat imports каналов (
TEAM_LIST) (v7 fix #36) - Не завязываться на
ServiceContextRegistry(данные Teams читаются из~/.claude, это локальный глобальный источник)
3.3 Wiring handlers.ts (v7 fix #48 — частично для list)
В этом проекте единая точка регистрации IPC — src/main/ipc/handlers.ts (initializeIpcHandlers()):
- Добавить импорт
initializeTeamHandlers/registerTeamHandlers/removeTeamHandlersиз./teams - Расширить сигнатуру
initializeIpcHandlers(...)новым параметромteamDataService - Внутри
initializeIpcHandlers():- вызвать
initializeTeamHandlers(teamDataService) - вызвать
registerTeamHandlers(ipcMain)
- вызвать
- В
removeIpcHandlers():- вызвать
removeTeamHandlers(ipcMain)
- вызвать
3.4 Preload bridge (Step 9 — частично)
В src/shared/types/api.ts добавить интерфейс TeamsAPI минимум с:
list(): Promise<TeamSummary[]>
И в ElectronAPI добавить поле:
teams: TeamsAPI
В src/preload/index.ts:
- Реализовать
api.teams.list()через существующийinvokeIpcWithResult<T>()(он уже используется дляconfig/ssh/context/httpServer) - Пробросить в
contextBridge.exposeInMainWorld()
Быстрая проверка до UI (делаем всегда):
- В renderer DevTools выполнить
await window.electronAPI.teams.list()и убедиться, что вернулся массив.
4) Renderer: Tabs + store + UI
4.1 unwrapIpc<T>() (Step 12)
Создать src/renderer/utils/unwrapIpc.ts:
- оборачивает вызов
api.teams.list() - не делает “double unwrap” (preload уже бросает ошибку при
success: false)
4.2 Tabs + UI entry (строго в рамках текущей модели табов)
Важно: в текущем коде Tab — это один интерфейс с optional sessionId/projectId и type-switch в местах использования. Полная миграция на discriminated union — отдельная refactor‑итерация (иначе итерация 1 перестанет быть “тонкой” и потеряет фокус на Team List).
В итерации 1 делаем ровно следующее:
-
В
src/renderer/types/tabs.ts:- расширить union
Tab['type'], добавив'teams' - ничего больше не меняем (никаких refactor’ов структуры Tab, никаких новых optional полей)
- расширить union
-
В
src/renderer/components/layout/PaneContent.tsx:- добавить ветку
tab.type === 'teams'→ рендерTeamListView
- добавить ветку
-
В
src/renderer/components/layout/SortableTab.tsx:- добавить иконку для
teamsвTAB_ICONS
- добавить иконку для
-
В
src/renderer/components/layout/TabBar.tsx:- добавить кнопку “Teams” (иконка
Usersизlucide-react) в ряд action‑кнопок - обработчик должен вызывать
openTeamsTab()(см. следующий пункт)
- добавить кнопку “Teams” (иконка
-
В store:
- добавить action
openTeamsTab()как per-pane singleton (строго по паттернуopenNotificationsTab()):- если в focused pane уже есть таб
type: 'teams'→ активируем его - иначе →
openTab({ type: 'teams', label: 'Teams' })(лейбл фиксируем какTeams, как уDashboard/Notifications)
- если в focused pane уже есть таб
- добавить action
CP2: pnpm typecheck после всех правок выше (до того, как писать TeamList UI).
4.3 teamSlice.fetchTeams() (Step 14 — частично)
Добавить src/renderer/store/slices/teamSlice.ts с минимумом:
teams: TeamSummary[]teamsLoading / teamsErrorfetchTeams(): вызываетunwrapIpc('team:list', () => api.teams.list())openTeamsTab(): per-pane singleton (описано в Step 4.2)
Никаких selectedTeamData, refresh generation и т.п. — это следующая итерация.
Подключить slice в src/renderer/store/types.ts и src/renderer/store/index.ts.
4.4 Tab integration + Teams view (Step 15 + Step 16 — частично)
- В
TabBarдобавить кнопку “Teams” → вызываетopenTeamsTab()(per-pane singleton) - В
PaneContentдобавить веткуtab.type === 'teams'→ рендерTeamListView
UI-компоненты (минимум для итерации 1):
src/renderer/components/team/TeamListView.tsx- на mount вызывает
fetchTeams() - показывает loading/error/empty
- рендерит список карточек/строк команд
- на mount вызывает
src/renderer/components/team/TeamEmptyState.tsx
Ручная проверка (обязательная для итерации 1, CP3)
- Запуск:
pnpm dev- Открыть вкладку
Teams
1.1) Singleton-поведение вкладки:
- Нажать кнопку “Teams” 2–3 раза
- Ожидание: не создаются новые табы, фокус остаётся на существующем
Teamsтабе (per-pane singleton)
- Empty state:
- Переименовать (временно)
~/.claude/teamsили протестировать на чистой машине - Ожидание: “Команды не найдены” (без ошибок в UI)
- Skip dirs без config:
- Создать папку
~/.claude/teams/default/безconfig.json - Ожидание: она не появляется в списке, UI не падает
- Happy path:
- Убедиться что реальная команда (где есть
config.json) отображается
Риски и как их гасим в рамках итерации
- Добавление нового
tab.type: обязательно обновитьPaneContent+SortableTab.TAB_ICONS, иначе получим runtime/TS ошибки. CP2 гарантирует, что всё “сшилось”. - Разный формат
config.json/ мусорные директории:TeamConfigReader.listTeams()делаетtry/catchиcontinue, не кидает исключения наружу. - Отсутствие
~/.claude/teams:listTeams()возвращает[]без ошибок. - Стабильность UI: в
TeamListViewдолжны быть 4 состояния: loading → data / empty / error.
Выходные артефакты (что появится после итерации)
- Документированный канал
TEAM_LISTиTeamsAPI.list() - Минимальный backend reader для списка команд
- Вкладка
Teamsи список команд в UI (или empty state) - База для следующих итераций без переархитектуривания