Files
kandev/docs/session-page-data-flow.md
T
2026-02-02 16:54:23 +00:00

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 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 <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 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.

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.
  • 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.