15 KiB
Итерация 04 — Messaging + Review (Inbox + ReviewDialog)
Эта итерация добавляет панель активности (inbox messages) и отправку сообщений тиммейтам, а также закрывает MVP review-flow: Request Review → Approve / Request Changes.
Основание:
docs/team-management/implementation.md(Iteration 4: Messaging + Review)docs/team-management/research-inbox.md(формат, race conditions, messageId verify)docs/team-management/research-messaging.md(почему inbox, ограничения доставки)docs/team-management/kanban-design.md(review flow, allowed transitions)docs/team-management/research-tasks.md(безопасная запись status при Fix)
Цель итерации
- Во вкладке Team появляется ActivityTimeline (история сообщений) и MessageComposer (отправка DM).
- Реализован review-flow:
- Request Review: перевод задачи в
REVIEW+ (если есть reviewer) уведомление в его inbox. - Approve: перевод задачи в
APPROVED. - Request Changes: опциональный комментарий → задача возвращается исполнителю:
- kanban-state запись удаляется (задача вернётся в
IN PROGRESSпоtask.status) task.statusстановитсяin_progress(atomic write + verify)- отправляется сообщение owner’у с описанием правок
- kanban-state запись удаляется (задача вернётся в
- Request Review: перевод задачи в
- Live refresh: изменения inbox/task/kanban отражаются в UI (уже существующий
team:change+ coalesce).
Не-цели (строго вне scope)
- Hard interrupt (прерывание mid-turn) — ограничение платформы, Phase 2.
- Архивация inbox / очистка истории / JSONL формат — Phase 2.
- Полноценный drag-and-drop для review — Phase 2 (пока click-to-move/кнопки).
- Round-robin балансировка reviewer’ов и reviewHistory — Phase 2.
- Любые write-path, связанные с созданием задач и
.highwatermark.
Контракт итерации (Main → Preload → Renderer)
Shared types (src/shared/types/team.ts)
Добавляем/расширяем:
InboxMessage(как вresearch-inbox.md):from: stringtext: string(plain text или JSON-строка)timestamp: string(ISO)read: booleansummary?: stringcolor?: stringmessageId?: string(в наших исходящих обязателен)
SendMessageRequest:member: stringtext: stringsummary?: stringfrom?: 'user' | string(по умолчанию'user')
SendMessageResult:deliveredToInbox: boolean(означает “записано в файл и verify прошёл”)messageId: string
Расширяем TeamData:
messages: InboxMessage[](агрегированные по всем inbox, отсортированные поtimestamp)
Расширяем ResolvedTeamMember:
status: 'active' | 'idle' | 'terminated' | 'unknown'lastActiveAt: string | nullmessageCount: numbercolor?: string
Правила статуса (из docs/team-management/README.md):
ACTIVE: last activity < 5 минутIDLE: last activity ≥ 5 минутTERMINATED: получен shutdown-event с approve=true (в доках встречаются разные названия, поэтому считаем terminated если structuredtextпосле JSON.parse имеет:type === 'shutdown_response' && approve === true, илиtype === 'shutdown_approved', илиtype === 'shutdown_response' && approved === true
- если данных нет →
unknown
IPC каналы
В src/preload/constants/ipcChannels.ts добавляем:
TEAM_SEND_MESSAGE = 'team:sendMessage'TEAM_REQUEST_REVIEW = 'team:requestReview'TEAM_UPDATE_KANBANуже существует с итерации 03 и расширяется (см. ниже) дляrequest_changes.
Важно: в v7-плане перечислены именно team:sendMessage, team:requestReview, team:updateKanban. В этой итерации не вводим новые каналы сверх этого набора.
TeamsAPI (src/shared/types/api.ts)
Расширяем:
sendMessage(teamName: string, req: SendMessageRequest): Promise<SendMessageResult>requestReview(teamName: string, taskId: string): Promise<void>updateKanban(teamName: string, taskId: string, patch: UpdateKanbanPatch): Promise<void>(из итерации 03, расширяем patch)
UpdateKanbanPatch — расширение в итерации 04 (без двусмысленностей)
К существующим операциям добавляем:
{ op: 'request_changes'; comment?: string }
Семантика request_changes (всё внутри main, как единая операция):
- Удалить kanban-state запись для taskId (remove)
- Обновить task файл:
status = 'in_progress'(atomic write + verify) - Отправить сообщение owner’у задачи с комментарием (если owner отсутствует → ошибка)
Definition of Done (DoD)
- Messaging UI
- В Team tab есть ActivityTimeline (список сообщений) и MessageComposer.
- Можно выбрать участника и отправить сообщение → появляется “Delivered” (или ошибка).
- Сообщение записывается в
~/.claude/teams/{team}/inboxes/{member}.jsonсmessageId.
- Review UI
- На задачах в
DONEесть действие “Request Review”. - В
REVIEWесть кнопки “Approve” и “Request Changes”. - “Request Changes” открывает диалог с опциональным комментарием.
- На задачах в
- Backend safety
- Inbox write: atomic write + verify
messageId+ retry/backoff (по плану ниже). - Task status write (Fix): atomic write + verify
status+ warning в UI при конфликте.
- Inbox write: atomic write + verify
- Качество
pnpm typecheckпроходит.pnpm testпроходит.
Выходные изменения (файлы)
Новые файлы
src/main/services/team/TeamInboxWriter.ts(write-path: atomic + verify + retry + lock)src/main/services/team/TeamTaskWriter.ts(минимум:updateStatus(teamName, taskId, status)), чтобы не смешивать чтение и записьsrc/renderer/components/team/activity/ActivityTimeline.tsxsrc/renderer/components/team/activity/ActivityItem.tsxsrc/renderer/components/team/activity/MessageComposer.tsxsrc/renderer/components/team/dialogs/ReviewDialog.tsx
Изменяемые файлы
src/shared/types/team.ts(InboxMessage, SendMessage*, member status,TeamData.messages)src/shared/types/api.ts(TeamsAPI methods)src/preload/constants/ipcChannels.ts(+ 3 канала)src/preload/index.ts(bridge для sendMessage/requestReview/updateKanban)src/main/ipc/guards.ts(+validateMemberName(),validateFromField()если потребуется)src/main/ipc/teams.ts(+ handlers)src/main/services/team/TeamInboxReader.ts(расширить:getMessagesFor(),getMessages())src/main/services/team/TeamMemberResolver.ts(status/messageCount/color/lastActiveAt + terminated detection)src/main/services/team/TeamDataService.ts(подтянуть messages + review methods; опираться на kanban из итерации 03; в requestReview — sendMessage первому reviewer при наличии)src/main/services/team/index.ts(exports)src/renderer/store/slices/teamSlice.ts(actions + sending states)src/renderer/components/team/TeamDetailView.tsx(layout: members + kanban + activity)src/main/services/infrastructure/FileWatcher.ts(ничего нового: inbox/task/kanban уже должны попадать вteam-change)
Порядок работ (runbook) с контрольными точками
CP0 — типы компилируются
- Обновить
src/shared/types/team.tsиsrc/shared/types/api.ts(контракты выше). pnpm typecheck
CP1 — Inbox read/write в main готов (без UI)
-
Inbox read
listInboxNames(teamName)(уже есть/или оставить) — список*.jsonбез расширенияgetMessagesFor(teamName, member):- ENOENT →
[] - invalid JSON →
[]+ warning
- ENOENT →
getMessages(teamName):- читает все inbox
*.json(параллельно, best-effort) - merge в один массив
- сортировка по
timestamp(desc)
- читает все inbox
-
Inbox write: atomic + verify + retry
Реализация должна следовать research-inbox.md:
- Каждое сообщение, которое пишет приложение, включает:
messageId = crypto.randomUUID()from = 'user'(по умолчанию)read = falsetimestamp = new Date().toISOString()
Алгоритм (в main):
read inbox JSON array (если нет → [])
append message
atomicWriteAsync(tmp + rename)
read back
verify messageId exists
если не найден → retry (до 3) с backoff 10/20/40ms
Параллелизм:
- внутри процесса сериализуем записи в один
inboxPathчерезwithInboxLock(inboxPath, fn), чтобы два IPC вызова не затёрли друг друга.
Fallback по from:
- делаем это не магией, а контрактом API:
SendMessageRequest.fromопционален, но если задан — валидируем (validateFromFieldили reusevalidateMemberName)- UI в этой итерации не обязано показывать выбор from; по умолчанию отправляем
from: 'user' - для отладки можно вызывать через DevTools
sendMessage(..., { from:'debug-user', ... })
pnpm test(если добавлялись новые unit-тесты) иpnpm typecheck
CP2 — Review actions в main + IPC
- Task status write (Fix)
Новый TeamTaskWriter.updateStatus(teamName, taskId, status):
- читает
~/.claude/tasks/{teamName}/{taskId}.json - меняет только
status - пишет через
atomicWriteAsync - перечитывает и проверяет, что
statusравен нужному - при конфликте (агент перезаписал) → throw, чтобы UI показал warning
- TeamDataService
getTeamData(teamName)теперь возвращает такжеmessagesrequestReview(teamName, taskId):updateKanban(taskId, { op:'set_column', column:'review' })(reviewStatus: pending)- выбрать reviewer:
- если
kanbanState.reviewers[]не пуст → первый элемент (round-robin — Phase 2) - иначе reviewer отсутствует (manual review)
- если
- если reviewer выбран →
sendMessage(reviewer, "...")
approveделаем напрямую черезupdateKanban(taskId, { op:'set_column', column:'approved' })request_changesделаем черезupdateKanban(taskId, { op:'request_changes', comment })(см. семантику patch выше)
- IPC
TEAM_SEND_MESSAGE: принимаетteamName,SendMessageRequestTEAM_REQUEST_REVIEW: принимаетteamName,taskIdTEAM_UPDATE_KANBAN: принимаетteamName,taskId,UpdateKanbanPatch(расширенный)
Guards:
validateMemberName()обязателен (защита от path traversal, member используется в пути inbox файла).validateTaskId()уже есть с итерации 03.
pnpm typecheck
CP3 — UI: ActivityTimeline + MessageComposer + ReviewDialog
- Layout
- Team tab становится 3-панельным:
- слева Members
- центр Kanban
- справа Activity (messages + composer)
Чтобы не потерять доступ к “сырому списку задач”, фиксируем простой UI-компромисс:
- правый panel становится табами:
ActivityиTasks - по умолчанию открываем
Activity, ноTasksостаётся доступным (и переиспользует текущийTaskList)
- ActivityTimeline
- Рендерит
TeamData.messages(последние N, например 200) с:- цветной dot (если есть
color) from,summary(или короткий previewtext)- timestamp (relative time)
- цветной dot (если есть
- MessageComposer
- select получателя (из
members) - textarea (минимум 1–2 строки)
- send → вызывает store action → IPC
sendMessage - состояния:
- sending / delivered / error
- ReviewDialog
- открывается при “Request Changes”
- comment optional
- submit →
updateKanban(..., { op:'request_changes', comment })
- Ручная проверка
- Отправить сообщение → файл inbox обновился, message появился в ActivityTimeline после refresh.
- Нажать Request Review → задача в REVIEW, при наличии reviewer’а в
kanban-state.reviewersему ушло сообщение. - Approve → задача в APPROVED.
- Request Changes → задача ушла исполнителю (status=in_progress) + отправлено сообщение owner’у.
pnpm test
Риски и митигации
- Race condition inbox: атомарная запись не решает overwrite race, поэтому делаем
messageId verify+ retry/backoff, плюс in-processwithInboxLock. - Конфликт при записи task.status: после write делаем verify; если agent перезаписал — показываем warning в UI, не делаем silent fail.
- Большие inbox: ограничиваем количество отображаемых сообщений (например 200) и добавляем “Show more” позже (итерация 05).