Parent Issue
Part of #500 — depends on #504, #505, #506, #507, #508
Context
This is the final integration page that assembles all the session UI components into a working interactive session view. It's the most important page from a user-experience perspective — it's where the bidirectional chat actually happens.
What to Build
New page: web-ui/src/app/sessions/[id]/page.tsx
Full-page layout:
┌─ Header ─────────────────────────────────────────────────────────┐
│ ← Sessions Session #a3f2 🟢 active $0.012 [End] │
├─ SplitPane (fills remaining viewport height) ────────────────────┤
│ │ │
│ AgentChatPanel │ AgentTerminal │
│ (45% default) │ (55% default) │
│ │ │
└──────────────────────────────────────────────────────────────────┘
Header:
- Back link "← Sessions" →
/sessions
- Session short ID
- State badge (🟢 active / ⚪ ended)
- Live cost counter (updates from
useAgentChat state)
- [End Session] button — calls
DELETE /api/v2/sessions/{id}, confirms, redirects to /sessions on success
- Disabled [End Session] if already ended
SplitPane:
- Left:
<AgentChatPanel sessionId={id} />
- Right:
<AgentTerminal sessionId={id} />
storageKey="session-split-{id}" so position is per-session
Ended session state:
- If session state is
ended: show AgentChatPanel in read-only mode (no input bar) and a banner: "This session has ended. [View history]"
AgentTerminal is hidden for ended sessions (or shows "Terminal unavailable — session has ended")
- Load message history from
GET /api/v2/sessions/{id}/messages (REST, not WS) for ended sessions
Loading state:
- Skeleton: header placeholder + two side-by-side gray boxes filling the viewport
Error state:
- Session not found: "Session not found" with link back to
/sessions
- WebSocket error: visible error banner inside
AgentChatPanel
Data flow:
- On mount:
GET /api/v2/sessions/{id} to get session metadata (state, model, workspace_path)
- If
active: mount AgentChatPanel + AgentTerminal (both connect via WS)
- If
ended: mount read-only AgentChatPanel with history from REST, no terminal
Page title:
<title>Session #{shortId} — CodeFRAME</title> via next/head or metadata
Acceptance Criteria
Notes
- This is a pure composition page — minimal logic beyond routing and passing
sessionId to child components
- Viewport height: use
calc(100vh - headerHeight) for the SplitPane so it fills the screen without scrolling
- Do not re-fetch session messages in both this page and
AgentChatPanel — let the chat panel own its message state via useAgentChat
Parent Issue
Part of #500 — depends on #504, #505, #506, #507, #508
Context
This is the final integration page that assembles all the session UI components into a working interactive session view. It's the most important page from a user-experience perspective — it's where the bidirectional chat actually happens.
What to Build
New page:
web-ui/src/app/sessions/[id]/page.tsxFull-page layout:
Header:
/sessionsuseAgentChatstate)DELETE /api/v2/sessions/{id}, confirms, redirects to/sessionson successSplitPane:
<AgentChatPanel sessionId={id} /><AgentTerminal sessionId={id} />storageKey="session-split-{id}"so position is per-sessionEnded session state:
ended: showAgentChatPanelin read-only mode (no input bar) and a banner: "This session has ended. [View history]"AgentTerminalis hidden for ended sessions (or shows "Terminal unavailable — session has ended")GET /api/v2/sessions/{id}/messages(REST, not WS) for ended sessionsLoading state:
Error state:
/sessionsAgentChatPanelData flow:
GET /api/v2/sessions/{id}to get session metadata (state, model, workspace_path)active: mountAgentChatPanel+AgentTerminal(both connect via WS)ended: mount read-onlyAgentChatPanelwith history from REST, no terminalPage title:
<title>Session #{shortId} — CodeFRAME</title>vianext/heador metadataAcceptance Criteria
AgentChatPanelis mounted and chat is functional for active sessionsAgentTerminalis mounted and terminal is functional for active sessionsSplitPanedivider is draggable; position persists per session/sessionsshows updated session stateNotes
sessionIdto child componentscalc(100vh - headerHeight)for the SplitPane so it fills the screen without scrollingAgentChatPanel— let the chat panel own its message state viauseAgentChat