# Task Session Page Data Flow (SSR + WS + Store) This document maps the current data flow for `/task/:id/:sessionId` and highlights which components/hooks depend on which data. It also calls out places where hooks could be made more granular or shared. ## SSR Entry Point Route: `apps/web/app/task/[id]/[sessionId]/page.tsx` Server fetches: - `fetchTask(taskId)` - `fetchBoardSnapshot(task.board_id)` - `listAgents()` - `listRepositories(task.workspace_id)` - `listTaskSessions(task.id)` for each task in board - `listTaskSessionMessages(sessionId)` (best-effort) Hydration: - `snapshotToState(snapshot)` → kanban columns + tasks - `taskToState(task, sessionId, messages?)` → `tasks.activeTaskId`, `tasks.activeSessionId`, `messages.bySession[sessionId]` - Repositories + agent profiles are merged into store - Render `` and `` ## Client Composition ``` TaskSessionPage (SSR) └─ TaskPageClient ├─ TaskTopBar └─ TaskLayout ├─ TaskSessionSidebar (left) ├─ TaskCenterPanel (chat/changes/notes) └─ TaskRightPanel (files + terminal) ``` ## Active Selection (Store) Store slice: `tasks.activeTaskId`, `tasks.activeSessionId` - Set on initial mount by `TaskPageClient` from URL + SSR task. - Updated when selecting a task/session in sidebar. ## WebSocket Data Flow WebSocket client: - `apps/web/lib/ws/client.ts` with deduplicated subscriptions. - Components call `client.subscribe(taskId)`; dedup ensures single WS subscription. - On reconnect, re-subscribes to active task channels. Handlers (global router): - `message.added` / `message.updated` → `messages.bySession[sessionId]` - `task_session.state_changed` → `taskSessionStatesByTaskId` + `taskSessions.items` - `git.status` → `gitStatus` (keyed by task) - `shell.output` → `shell.outputs[taskId]` ## Component Data Dependencies ### TaskPageClient (`apps/web/app/task/[id]/page-client.tsx`) - Props: `task`, `sessionId`, `initialSessionsByTask`, `initialRepositories`, `initialAgentProfiles` - Store: - `tasks.activeTaskId`, `tasks.activeSessionId` via `setActiveSession` - `kanban.tasks`, `kanban.columns`, `workspaces.items` - `agentProfiles.items` - Hooks: - `useSessionResumption(task?.id, sessionId)` - `useTaskAgent(task)` (agent state, task.execution polling) - `useRepositories(workspaceId)` (fallback to SSR data) - WS: - Subscribes to all tasks in board (deduped) - Listens for `task_session.state_changed` to refresh `sessionsByTask` - Responsibilities: - URL sync via `history.replaceState` - Task selection triggers `fetchTask` + `listTaskSessions` ### TaskSessionSidebar (`apps/web/components/task/task-session-sidebar.tsx`) - Store: - `tasks.activeTaskId` - `tasks.activeSessionId` - Props: - tasks + columns + sessionsByTask from `TaskPageClient` - Uses: - `TaskSessionSwitcher` (sessions list) - `TaskSwitcher` (tasks list) - Calls back to parent for selection and session list reload. ### TaskCenterPanel (`apps/web/components/task/task-center-panel.tsx`) - Props: `taskId`, `onSendMessage`, `selectedDiffPath`, `openFileRequest` - Includes: - `TaskChatPanel` - `TaskChangesPanel` - `FileViewerContent` ### TaskChatPanel (`apps/web/components/task/task-chat-panel.tsx`) - Store: - `tasks.activeSessionId` (fallback unless `sessionId` prop passed) - Hooks: - `useTaskSession(sessionId)` → taskId + session details - `useTaskMessages(taskId, sessionId)` → messages + meta - `useLazyLoadMessages(sessionId)` → pagination - Data: - Renders from `messages.bySession[sessionId]` ### TaskRightPanel (`apps/web/components/task/task-right-panel.tsx`) - Store: - `tasks.activeSessionId` - Uses: - `ShellTerminal taskId + activeSessionId` ### ShellTerminal (`apps/web/components/task/shell-terminal.tsx`) - Store: - `shell.outputs[taskId]` - `taskSessions.items[sessionId]?.state` or `taskSessionStatesByTaskId[taskId]` - WS: - Subscribes to shell only when session is RUNNING or WAITING_FOR_INPUT. ### TaskFilesPanel / TaskChangesPanel - Store: - `gitStatus` keyed by active task - No direct WS subscriptions. ## ASCII Diagram (Current) ``` SSR ┌──────────────────────────────────────────────────────────────┐ │ /task/:id/:sessionId │ │ fetchTask + snapshot + agents + repos + sessions + messages │ │ StateHydrator -> store (activeTaskId + activeSessionId) │ └──────────────────────────────────────────────────────────────┘ Client ┌─────────────────────────────── TaskPageClient ──────────────────────────────┐ │ setActiveSession(taskId, sessionId) │ │ subscribe(board tasks) + listen task_session.state_changed │ │ │ │ ┌─ TaskSessionSidebar (sessions + tasks) │ │ ├─ TaskCenterPanel │ │ │ └─ TaskChatPanel │ │ │ ├─ useTaskSession(sessionId) │ │ │ ├─ useTaskMessages(taskId, sessionId) │ │ │ └─ useLazyLoadMessages(sessionId) │ │ └─ TaskRightPanel │ │ └─ ShellTerminal (shell.output + session state) │ └──────────────────────────────────────────────────────────────────────────────┘ WS task.subscribe -> deduped in client message.added/updated -> messages.bySession task_session.state_changed -> taskSessions + taskSessionStatesByTaskId git.status -> gitStatus (task) shell.output -> shell.outputs[task] ``` ## Hooks Review (Granularity + Reuse) Current hooks: - `useTaskMessages(taskId, sessionId)` - Fetches + sets loading meta + subscribes to task updates. - Could be split into: - `useSessionMessages(sessionId)` (only fetch + store) - `useTaskSubscription(taskId)` (subscription only) - `useLazyLoadMessages(sessionId)` uses store meta; OK as a focused pagination hook. - `useTaskSession(sessionId)` derives taskId from messages if session not in store. - Can be simplified once session list is guaranteed in store. - `useTaskAgent(task)` + `useSessionResumption(taskId, sessionId)` - Both access execution state. Consider a combined `useTaskExecution(taskId, sessionId)` hook. - `useRepositories(workspaceId)` currently handles fetch + loading; OK but could be generalized (e.g. `useListResource`). Suggested split for sharing: - `useTaskSubscription(taskId)` → handles `client.subscribe(taskId)` only. - `useSessionMessages(sessionId)` → fetch + store; no taskId. - `useActiveSession()` selector → centralizes active session usage. - `useTaskGitStatus(taskId)` → pure selector + optional subscribe. - `useShellOutput(taskId)` → pure selector + optional subscribe. This would allow panels to mix and match only what they need without pulling in extra behavior.