7.6 KiB
7.6 KiB
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 boardlistTaskSessionMessages(sessionId)(best-effort)
Hydration:
snapshotToState(snapshot)→ kanban columns + taskstaskToState(task, sessionId, messages?)→tasks.activeTaskId,tasks.activeSessionId,messages.bySession[sessionId]- Repositories + agent profiles are merged into store
- Render
<StateHydrator initialState={...} />and<TaskPageClient ... />
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
TaskPageClientfrom URL + SSR task. - Updated when selecting a task/session in sidebar.
WebSocket Data Flow
WebSocket client:
apps/web/lib/ws/client.tswith 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.itemsgit.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.activeSessionIdviasetActiveSessionkanban.tasks,kanban.columns,workspaces.itemsagentProfiles.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_changedto refreshsessionsByTask
- Responsibilities:
- URL sync via
history.replaceState - Task selection triggers
fetchTask+listTaskSessions
- URL sync via
TaskSessionSidebar (apps/web/components/task/task-session-sidebar.tsx)
- Store:
tasks.activeTaskIdtasks.activeSessionId
- Props:
- tasks + columns + sessionsByTask from
TaskPageClient
- tasks + columns + sessionsByTask from
- 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:
TaskChatPanelTaskChangesPanelFileViewerContent
TaskChatPanel (apps/web/components/task/task-chat-panel.tsx)
- Store:
tasks.activeSessionId(fallback unlesssessionIdprop passed)
- Hooks:
useTaskSession(sessionId)→ taskId + session detailsuseTaskMessages(taskId, sessionId)→ messages + metauseLazyLoadMessages(sessionId)→ pagination
- Data:
- Renders from
messages.bySession[sessionId]
- Renders from
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]?.stateortaskSessionStatesByTaskId[taskId]
- WS:
- Subscribes to shell only when session is RUNNING or WAITING_FOR_INPUT.
TaskFilesPanel / TaskChangesPanel
- Store:
gitStatuskeyed 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.
- Both access execution state. Consider a combined
useRepositories(workspaceId)currently handles fetch + loading; OK but could be generalized (e.g.useListResource).
Suggested split for sharing:
useTaskSubscription(taskId)→ handlesclient.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.