- Introduced new parameters for cross-team messaging, including CROSS_TEAM_SENT_SOURCE for better tracking of sent messages. - Updated sendCrossTeamMessage function to append sent messages to the message store, ensuring a complete history of communications. - Enhanced tests to validate the new message storage functionality and ensure accurate retrieval of sent messages. - Improved handling of message timestamps and deduplication logic for cross-team communications.
9.7 KiB
Split Screen Multi-View Research
Исследование: поддержка одновременного просмотра нескольких сессий/команд в split pane. Дата: 2026-03-10
Текущее состояние архитектуры
Split Pane System (уже реализовано)
- До 4 панелей одновременно (
MAX_PANES = 4вsrc/renderer/types/panes.ts) - Drag-and-drop между панелями (dnd-kit,
TabbedLayout.tsx) - Resize handles между панелями (
PaneResizeHandle.tsx) - CSS
display: nonetoggle — все вкладки mounted, только active видна (PaneContent.tsx) TabUIContextпредоставляетtabIdпотомкам
Pane Layout Structure
// src/renderer/types/panes.ts
interface Pane {
id: string;
tabs: Tab[];
activeTabId: string;
selectedTabIds: string[];
widthFraction: number; // 0-1, сумма всех = 1.0
}
interface PaneLayout {
panes: Pane[];
focusedPaneId: string; // какая панель в фокусе
}
Backward Compatibility Facade
Root-level openTabs, activeTabId, selectedTabIds синхронизируются из focused pane only через syncFromLayout() в tabSlice.ts.
Изоляция состояния: что per-tab vs глобальное
✅ Per-Tab (уже изолировано)
| Состояние | Хранение | Слайс |
|---|---|---|
| UI expansion state | tabUIStates[tabId] |
tabUISlice |
| Scroll position | tabUIStates[tabId].savedScrollTop |
tabUISlice |
| Context panel visibility | tabUIStates[tabId].showContextPanel |
tabUISlice |
| Context phase selection | tabUIStates[tabId].selectedContextPhase |
tabUISlice |
| Session data cache | tabSessionData[tabId] |
sessionDetailSlice |
| Conversation cache | tabSessionData[tabId].conversation |
sessionDetailSlice |
Паттерн чтения:
const stats = useStore((s) => {
const td = tabId ? s.tabSessionData[tabId] : null;
return td?.sessionClaudeMdStats ?? s.sessionClaudeMdStats;
});
❌ Глобальное (проблемы для multi-view)
| Состояние | Слайс | Проблема |
|---|---|---|
selectedTeamName |
teamSlice |
Одна команда на всё приложение |
selectedTeamData |
teamSlice |
Полные данные только одной команды |
searchQuery |
conversationSlice |
Поиск общий для всех вкладок |
searchVisible |
conversationSlice |
Показ поиска общий |
searchMatches |
conversationSlice |
Результаты поиска общие |
currentSearchIndex |
conversationSlice |
Навигация по результатам общая |
expandedAIGroupIds |
conversationSlice |
Legacy дубль tabUISlice |
expandedDisplayItemIds |
conversationSlice |
Legacy дубль tabUISlice |
expandedStepIds |
conversationSlice |
Глобальное, логично per-tab |
activeDetailItem |
conversationSlice |
Глобальное, логично per-tab |
⚠️ Синхронизируемое (работает через swap)
| Состояние | Механизм |
|---|---|
selectedProjectId |
Swap при фокусе pane |
selectedSessionId |
Swap при фокусе pane |
sessionDetail (global) |
Swap из tabSessionData[tabId] |
conversation (global) |
Swap из tabSessionData[tabId] |
Варианты реализации
Вариант A: Полная поддержка split-screen для сессий
Надёжность: 8/10 | Уверенность: 9/10
Основа уже заложена через tabSessionData. Нужно:
-
Search isolation (~5 файлов):
- Перенести
searchQuery,searchVisible,searchMatches,currentSearchIndexвtabUISlice - Обновить
SearchBar,useSearchContextNavigation,searchHighlightUtils - Компоненты читают search state через
tabUIStates[tabId]
- Перенести
-
Legacy cleanup (~3 файла):
- Удалить
expandedAIGroupIdsиexpandedDisplayItemIdsизconversationSlice - Убедиться все компоненты используют
tabUISliceверсии - Удалить
expandedStepIdsиз global scope
- Удалить
-
Верификация (~3 файла):
- Проверить все компоненты в chat/ читают через
tabSessionData[tabId]паттерн - Проверить что
activeDetailItemизолирован
- Проверить все компоненты в chat/ читают через
Объём: ~8-12 файлов, средняя сложность.
Вариант B: Полная поддержка split-screen для команд
Надёжность: 7/10 | Уверенность: 7/10
Нужна новая инфраструктура:
-
Per-tab team data cache (~5 файлов):
// В teamSlice или sessionDetailSlice tabTeamData: Record<string, { teamName: string; teamData: TeamData | null; loading: boolean; error: string | null; }> -
selectTeam() с tabId (~3 файла):
selectTeam(teamName, tabId?)— кэширует вtabTeamData[tabId]- При переключении tab: swap из кэша или fetch
- При закрытии tab: cleanup кэша
-
Team компоненты (~8 файлов):
TeamDetailView,TeamChatView,TeamKanbanViewи др.- Читать через
tabTeamData[tabId]паттерн - File watcher: обновлять нужные tab кэши
-
Sidebar sync (~2 файла):
- При фокусе pane с team tab: sync sidebar к этой команде
Объём: ~15-20 файлов, высокая сложность.
Вариант C: A + B (полный split-screen)
Надёжность: 6/10 | Уверенность: 7/10
Объём: ~20-25 файлов.
Риски
Высокие
- Race conditions при file watcher events — обновление прилетает, нужно обновить правильный tab cache. Для сессий решено через
tabFetchGenerationMap, для команд нужен аналог. - Search isolation — search завязан на глобальные
searchMatchesи навигацию по ним, самый трудоёмкий рефактор.
Средние
- Memory pressure — каждый tab хранит полный кэш. Для сессий работает (cleanup при закрытии). Для команд нужен аналог.
- Sidebar sync — сайдбар показывает контекст focused pane. При переключении нужен корректный swap project/worktree/team.
- Stale data — два tab с одной сессией/командой: file watcher обновляет оба или только active?
Низкие
- DnD between panes — перетаскивание team tab между panes должно триггерить cache transfer.
- Tab duplication —
openTab()проверяет дупликаты across ALL panes. Нужно ли разрешить одну и ту же команду в двух panes?
Ключевые файлы
Store Slices
| Файл | Роль |
|---|---|
src/renderer/store/slices/tabSlice.ts |
Tab lifecycle, session switching, backward compat |
src/renderer/store/slices/paneSlice.ts |
Multi-pane split/resize/focus |
src/renderer/store/slices/tabUISlice.ts |
Per-tab UI state (expansion, scroll) |
src/renderer/store/slices/sessionDetailSlice.ts |
Session data + per-tab caching |
src/renderer/store/slices/conversationSlice.ts |
Search, legacy expansion (нужен рефактор) |
src/renderer/store/slices/teamSlice.ts |
Team selection (глобальное, нужен рефактор) |
Layout Components
| Файл | Роль |
|---|---|
src/renderer/components/layout/TabbedLayout.tsx |
Main layout + DnD context |
src/renderer/components/layout/TabBarRow.tsx |
Full-width tab bar (pane-proportional) |
src/renderer/components/layout/TabBar.tsx |
Single pane tab bar |
src/renderer/components/layout/PaneContainer.tsx |
Split layout renderer |
src/renderer/components/layout/PaneView.tsx |
Single pane wrapper |
src/renderer/components/layout/PaneContent.tsx |
Tab content renderer (display-toggle) |
src/renderer/components/layout/SessionTabContent.tsx |
Session tab content |
Contexts
| Файл | Роль |
|---|---|
src/renderer/contexts/TabUIContext.tsx |
Per-tab ID provider |
src/renderer/contexts/useTabUIContext.ts |
Context hook |
Рекомендация
Начать с Варианта A (сессии в split-screen):
- 80% инфраструктуры уже есть
- Нужно дочистить search isolation и legacy duplicates
- Низкий риск регрессий
Затем Вариант B (команды):
- Когда паттерн per-tab caching отработан на сессиях
- Применить тот же подход к team data
Обнаруженные баги (побочный результат ресёрча)
- Search state не изолирован — поиск в одной вкладке влияет на другие
- Legacy дублирование —
expandedAIGroupIdsсуществует и вconversationSliceи вtabUISlice - Team tabs в split pane — обе панели показывают одну команду (последнюю выбранную)