6.5 KiB
6.5 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
flowchart TD
A["TaskSessionPage (SSR)"] --> B["TaskPageClient"]
B --> C["TaskTopBar"]
B --> D["TaskLayout"]
D --> E["TaskSessionSidebar (left)"]
D --> F["TaskCenterPanel (chat/changes/notes)"]
D --> G["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.
Data Flow Diagram
flowchart TB
subgraph SSR["SSR: /task/:id/:sessionId"]
fetch["fetchTask + snapshot + agents + repos + sessions + messages"]
hydrate["StateHydrator → store"]
end
subgraph Client["TaskPageClient"]
setActive["setActiveSession(taskId, sessionId)"]
subscribe["subscribe(board tasks)"]
subgraph Components
sidebar["TaskSessionSidebar"]
center["TaskCenterPanel"]
right["TaskRightPanel"]
end
subgraph ChatPanel["TaskChatPanel"]
useSession["useTaskSession(sessionId)"]
useMessages["useTaskMessages(taskId, sessionId)"]
useLazy["useLazyLoadMessages(sessionId)"]
end
shell["ShellTerminal"]
end
subgraph WS["WebSocket Events"]
ws1["task.subscribe → deduped"]
ws2["message.added/updated → messages.bySession"]
ws3["task_session.state_changed → taskSessions"]
ws4["git.status → gitStatus"]
ws5["shell.output → shell.outputs"]
end
SSR --> Client
center --> ChatPanel
right --> shell
WS --> Client
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.