Skip to content

feat(web-ui): /sessions list page + sidebar navigation entry (#508)#519

Merged
frankbria merged 10 commits into
mainfrom
feature/issue-508-sessions-list-page
Apr 2, 2026
Merged

feat(web-ui): /sessions list page + sidebar navigation entry (#508)#519
frankbria merged 10 commits into
mainfrom
feature/issue-508-sessions-list-page

Conversation

@frankbria
Copy link
Copy Markdown
Owner

@frankbria frankbria commented Apr 2, 2026

Summary

Implements #508 — adds the /sessions page and sidebar navigation entry so users can discover and manage interactive agent sessions.

  • Session types: Session, SessionState, SessionListResponse added to src/types/index.ts
  • sessionsApi: getAll, create, end methods added to src/lib/api.ts
  • SessionCard: state dot (green/gray), short ID, workspace name, model, relative time, cost, Resume/View/End buttons
  • NewSessionModal: dialog with workspace path input, model selector (sonnet/opus/haiku), loading state, form reset on reopen
  • SessionListView: SWR fetch (10s revalidation), loading skeletons, empty state, error state with retry, search filter, active-first sort
  • /sessions page: workspace hydration guard following existing page pattern
  • AppSidebar: Sessions nav entry (CommandLineIcon), active session count badge (blue, 30s poll)

Acceptance Criteria

  • /sessions page lists sessions from API, sorted active first
  • Session cards show state, model, elapsed time, and cost
  • [Resume →] / [View →] links navigate to /sessions/[id]
  • [End] button ends an active session with confirmation
  • "New Session" modal creates a session and redirects to detail page
  • Empty state shown when no sessions exist
  • Loading skeletons shown during initial fetch
  • Sidebar nav entry visible, active-highlighted when on /sessions
  • Sidebar badge shows count of active sessions

Test Plan

  • 112 tests written (TDD approach) across SessionCard, NewSessionModal, SessionListView, AppSidebar
  • All 112 tests passing
  • Linting clean (0 errors introduced)
  • Code review passed — 3 issues found and fixed (error handling, stale modal state, SWR key encoding)

Implementation Notes

  • Sessions API calls fall back gracefully — handleEnd shows an inline error banner on failure and always re-syncs SWR
  • NewSessionModal resets workspace path and model on each open (prevents stale state across workspace switches)
  • SWR cache keys use encodeURIComponent(workspacePath) to handle paths with special characters correctly
  • CodeRabbit CLI was not authenticated; internal code review served as the primary gate

Closes #508

Summary by CodeRabbit

  • New Features

    • Sessions management UI: list, create modal, and a placeholder session detail page.
    • Sidebar adds a Sessions item with an active-session numeric badge.
    • Session cards show status, relative time, workspace, model, cost, and Resume/View/End actions.
    • Search and “New Session” quick action; create flow navigates to new session.
  • Tests

    • Added comprehensive tests for session components and pages; fixed test-state leakage.
  • Documentation

    • README updated to mention Sessions list with active-session badge.

Test User added 8 commits April 1, 2026 18:30
Add TypeScript types for the sessions feature (#508):
- SessionState: 'active' | 'ended' | 'paused'
- Session: id, state, workspace_path, model, timestamps, cost, agent_name
- SessionListResponse: sessions array with total count
Add sessions API client with getAll, create, and end methods
following the existing API client pattern (#508).
SessionCard displays session state dot, short ID, workspace name,
model, cost, relative time, and action buttons (Resume/View/End).
16 tests covering all display and interaction scenarios.
Dialog modal for creating new sessions with workspace path input,
model selector (sonnet/opus/haiku), and loading state handling.
7 tests covering render, defaults, submit, and loading behavior.
Main sessions page content with SWR data fetching, loading skeletons,
empty state, error state, search filtering, sorting (active first),
and NewSessionModal integration. 7 tests covering all states.
Thin page wrapper following tasks/page.tsx pattern with workspace
hydration guard, no-workspace fallback, and SessionListView render.
Add Sessions link between Execution and Blockers in sidebar navigation
with CommandLineIcon. Active session count badge (blue) polled every 30s.
3 new tests + updated nav count test (6→8 items).
- handleEnd: wrap in try/catch, surface error banner, always mutate in finally
- handleCreate: rename param to avoid shadowing useSWR data var
- NewSessionModal: reset form on open to prevent stale workspace path
- AppSidebar/SessionListView: encode workspacePath in SWR cache keys
- Update AppSidebar test keys to match encodeURIComponent output
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ad838aa2-b09c-4c42-bd73-b169451638e4

📥 Commits

Reviewing files that changed from the base of the PR and between 470300d and 9ad9390.

📒 Files selected for processing (1)
  • README.md
✅ Files skipped from review due to trivial changes (1)
  • README.md

Walkthrough

Adds a Sessions feature: new /sessions pages and detail stub, SessionListView, SessionCard, NewSessionModal, types and sessionsApi, sidebar nav entry with active-session badge, and comprehensive tests for the new UI and sidebar behavior.

Changes

Cohort / File(s) Summary
Session UI Components
web-ui/src/components/sessions/SessionCard.tsx, web-ui/src/components/sessions/SessionListView.tsx, web-ui/src/components/sessions/NewSessionModal.tsx
New client components: SessionCard (display, end action), SessionListView (SWR fetching, search, sorting, create/end flows, error/empty/loading states), and NewSessionModal (controlled form, submit handling, disabled state while submitting).
Pages / Routing
web-ui/src/app/sessions/page.tsx, web-ui/src/app/sessions/[id]/page.tsx
Added /sessions list page (reads selected workspace, avoids hydration flash) and a session detail page stub showing last-8-id and back link.
Sidebar / Navigation & Tests
web-ui/src/components/layout/AppSidebar.tsx, web-ui/__tests__/components/layout/AppSidebar.test.tsx
Inserted "Sessions" nav item with icon and SWR-driven active-session count (30s poll). Updated AppSidebar test SWR mock to be key-aware and adjusted expected nav items/count (now includes Sessions).
API & Types
web-ui/src/lib/api.ts, web-ui/src/types/index.ts
Added sessionsApi (getAll/create/end) and new exported types: SessionState, Session, SessionListResponse, SessionCreateRequest.
Session Tests
web-ui/__tests__/components/sessions/NewSessionModal.test.tsx, web-ui/__tests__/components/sessions/SessionCard.test.tsx, web-ui/__tests__/components/sessions/SessionListView.test.tsx
New comprehensive test suites covering modal form behavior and submission, session card rendering and end confirmation, list view states (loading/empty/error/search/sorting), and SWR/api interactions.
Docs
README.md
Updated "What Works Today" to include "Sessions list with active-session badge".

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Page as Sessions Page
    participant List as SessionListView
    participant SWR as SWR Layer
    participant API as sessionsApi
    participant Backend as /api/v2/sessions

    User->>Page: Navigate to /sessions
    Page->>List: render with workspacePath
    List->>SWR: useSWR(getAll workspacePath)
    SWR->>API: sessionsApi.getAll(workspacePath)
    API->>Backend: GET /api/v2/sessions?workspace_path=...
    Backend-->>API: sessions[]
    API-->>SWR: SessionListResponse
    SWR-->>List: { sessions, isLoading, error }
    List-->>User: render session cards (sorted, filtered)

    User->>List: Click "+ New Session"
    List->>User: Open NewSessionModal
    User->>Modal: submit {workspace_path, model}
    Modal->>API: sessionsApi.create(data)
    API->>Backend: POST /api/v2/sessions
    Backend-->>API: Session
    API-->>Modal: Session
    Modal->>List: close + trigger mutate()
    List->>SWR: mutate() -> refetch
    SWR->>API: getAll(...)
    Backend-->>SWR: Updated sessions[]
    SWR-->>List: updated list
    List-->>User: show new session

    User->>Card: Click "End"
    Card->>User: window.confirm()
    User-->>Card: confirm true
    Card->>API: sessionsApi.end(id)
    API->>Backend: DELETE /api/v2/sessions/{id}
    Backend-->>API: success
    API-->>List: (void)
    List->>SWR: mutate()
    SWR-->>List: refreshed sessions
    List-->>User: updated UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hop, hop — sessions now take flight,

Cards and modals gleam at night,
Sidebar counts each lively run,
NewSession hums — a job well done,
Tests tuck every burrow tight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly and concisely summarizes the main changes: adding a /sessions list page and sidebar navigation entry for managing sessions.
Linked Issues check ✅ Passed All acceptance criteria from issue #508 are met: sessions page with sorting, session cards with state/model/time/cost, Resume/View/End actions, New Session modal, empty/error/loading states, sidebar navigation with active-session badge.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the /sessions feature specified in #508; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-508-sessions-list-page

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 2, 2026

Code Review — Sessions List Page + Sidebar Navigation (#508)

Good overall implementation. The TDD approach with 112 tests is well-executed, and the component architecture (types → api → card → list → page) follows the existing patterns cleanly. A few things worth addressing:

Bug: isSubmitting not reset on modal close

NewSessionModal.tsx — The useEffect that resets form state on open only resets workspacePath and model, but not isSubmitting. If a user closes the dialog while a submission is in-flight (e.g. hits Escape), then reopens it, the "Start Session" button will be permanently disabled until the original Promise resolves. Add setIsSubmitting(false) to the reset effect:

useEffect(() => {
  if (open) {
    setWorkspacePath(defaultWorkspacePath);
    setModel(MODEL_OPTIONS[0]);
    setIsSubmitting(false); // add this
  }
}, [open, defaultWorkspacePath]);

UX: cost_usd rendered without number formatting

SessionCard.tsx — The cost is rendered as a raw number which will show floating-point output (e.g. $0.012000000001) for values that don't have clean string representations. The test passes because 0.012 is a clean decimal, but real API values may not be. Suggest .toFixed(4) or a locale-aware formatter.

Nit: SWR cache key doesn't match the actual API endpoint (AppSidebar)

The SWR key uses /api/v2/sessions/sidebar?path=... but the fetcher calls sessionsApi.getAll() which targets /api/v2/sessions. This is harmless (SWR uses the key only for cache identity, the fetcher is authoritative), but mismatched keys make cache debugging confusing. Consider aligning the key with what the fetcher actually targets, e.g. /api/v2/sessions?path=...&state=active.

Nit: endError not cleared before a retry attempt

SessionListView.tsxhandleEnd doesn't call setEndError(null) before starting a new attempt. If one End fails and shows an error banner, a second End on the same or different session will leave the stale error visible until the new attempt completes. One-liner fix: add setEndError(null) at the top of handleEnd.

Minor: model state type could be tighter

In NewSessionModal.tsx, MODEL_OPTIONS is as const but model state is typed as string. Typing it as typeof MODEL_OPTIONS[number] would catch any invalid model string at compile time.

Architecture note

Verify that /api/v2/sessions (GET + POST) and /api/v2/sessions/{id} (DELETE) are wired up in a v2 router before merge — otherwise the page works in tests but breaks in production.


The isSubmitting bug and cost formatting are worth fixing before merge. The rest are nits.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web-ui/__tests__/components/layout/AppSidebar.test.tsx (1)

53-56: ⚠️ Potential issue | 🟡 Minor

Reset mockSWRData in beforeEach to prevent cross-test leakage.

jest.clearAllMocks() does not clear entries in mockSWRData, so data set in one test can affect later tests.

🔧 Suggested fix
 beforeEach(() => {
   jest.clearAllMocks();
   mockUsePathname.mockReturnValue('/');
+  Object.keys(mockSWRData).forEach((key) => {
+    delete mockSWRData[key];
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/__tests__/components/layout/AppSidebar.test.tsx` around lines 53 - 56,
The tests leak SWR state because mockSWRData isn't cleared between runs; update
the test suite's beforeEach (the existing beforeEach that calls
jest.clearAllMocks() and mockUsePathname.mockReturnValue('/')) to also reset or
reinitialize mockSWRData (e.g., set it to an empty object or call its reset
method) so each test starts with a clean SWR mock state; reference mockSWRData
and the beforeEach block when making the change.
🧹 Nitpick comments (2)
web-ui/__tests__/components/sessions/SessionListView.test.tsx (1)

142-153: Exercise the actual retry action, not just button visibility.

This test currently verifies the Retry button is rendered but not that clicking it calls mutate. Adding that assertion would close the loop on retry behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/__tests__/components/sessions/SessionListView.test.tsx` around lines
142 - 153, The test shows the Retry button but doesn't assert its behavior;
update the 'shows error state with retry button' test to simulate a user click
on the Retry button and assert that the mocked mutate function is called. Keep
using the existing mockUseSWR return value (with const mutate = jest.fn()) and
SessionListView render, then use userEvent.click or fireEvent.click on
screen.getByRole('button', { name: /retry/i }) and
expect(mutate).toHaveBeenCalled();.
web-ui/src/components/sessions/SessionListView.tsx (1)

158-158: Use Shadcn Button for the dismiss action.

Line 158 uses a raw <button> in web-ui/src, which diverges from the UI component rule used elsewhere in this file.

🎯 Suggested change
- <button className="ml-2 underline" onClick={() => setEndError(null)}>Dismiss</button>
+ <Button
+   variant="link"
+   size="sm"
+   className="ml-2 h-auto p-0"
+   onClick={() => setEndError(null)}
+ >
+   Dismiss
+ </Button>

As per coding guidelines: "web-ui/src/**/*.{ts,tsx}: Web UI must use Shadcn/UI (Nova preset) with gray color scheme and Hugeicons (@hugeicons/react); never use lucide-react".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/sessions/SessionListView.tsx` at line 158, Replace the
raw HTML <button> used for the dismiss action in SessionListView.tsx with the
Shadcn Button component (e.g., Button from your shadcn/ui bundle) and wire the
same onClick handler (setEndError(null)); remove the underline class and instead
use the appropriate Button variant/props (e.g., a link/ghost style or gray color
scheme) plus the ml-2 spacing so visual layout is preserved, and add the Button
import to the top of the file if missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/src/components/layout/AppSidebar.tsx`:
- Around line 126-129: The Sessions badge currently hardcodes a blue token
("bg-blue-500") in the JSX where label === 'Sessions' and activeSessionCount is
rendered; update the className for the badge (the span that renders
{activeSessionCount}) to use the project's Shadcn/UI Nova gray theme tokens
instead of bg-blue-500 (e.g., replace blue token with the corresponding gray
background and matching gray/white text tokens from the Nova preset such as
bg-gray-500/600 and text-gray-900 or text-white as appropriate) so it follows
the gray color scheme used across web-ui components.

In `@web-ui/src/components/sessions/NewSessionModal.tsx`:
- Line 33: Extract the inline payload type used by NewSessionModal's onSubmit
((data: { workspace_path: string; model: string }) => Promise<void>) into a
shared exported interface named SessionCreateRequest in
web-ui/src/types/index.ts, export it, then import and use that type in
NewSessionModal (replace the inline shape on onSubmit) and in SessionListView so
both components consume the same SessionCreateRequest type; update any affected
function signatures (e.g., onSubmit) and imports accordingly.

In `@web-ui/src/components/sessions/SessionCard.tsx`:
- Around line 51-55: The current nesting of Link > Button creates an interactive
element inside another; change to Button asChild wrapping Link so Button renders
the Link element instead. In SessionCard, replace the Link wrapping Button with
Button having the prop asChild and put <Link
href={`/sessions/${session.id}`}>...</Link> as its child, preserving Button
props (size="sm" variant="ghost" className="h-7 gap-1 px-2 text-xs") and the
label that uses isActive ? 'Resume' : 'View' + " →".

In `@web-ui/src/components/sessions/SessionListView.tsx`:
- Around line 50-58: handleEnd currently sets endError in the catch but never
clears it on success; update the handleEnd function so that after a successful
await sessionsApi.end(id) you call setEndError('') (or null) to clear the stale
error before calling mutate(), and optionally capture the caught error in catch
(e) to log or ignore; reference: handleEnd, setEndError, sessionsApi.end,
mutate.
- Line 64: Add the missing dynamic session detail route for the /sessions/:id
path by creating a route component that accepts the route parameter id (e.g.,
export default async function SessionPage({ params }) { ... }) and fetches the
session by params.id, rendering the session details (or a not-found state) so
router.push(`/sessions/${session.id}`) resolves; ensure the component handles
create/resume/view flows, returns appropriate JSX/ReactNode, and exports
metadata or error handling as needed to match existing sessions list behavior.

In `@web-ui/src/lib/api.ts`:
- Around line 634-645: sessionsApi.getAll currently assumes a { sessions, total
} shape but the backend returns a list response; update getAll and its types so
the returned value matches the backend contract: adjust the SessionListResponse
type to the actual list response shape returned by GET /api/v2/sessions (e.g.,
an array or ListResponse<T> as used elsewhere) and return response.data exactly
(or map response.data.results to the expected shape) from getAll instead of
fabricating { sessions, total }; update any callers expecting
sessionData.sessions to use the corrected property name (e.g., the array itself
or .results) so runtime undefined errors are resolved.

---

Outside diff comments:
In `@web-ui/__tests__/components/layout/AppSidebar.test.tsx`:
- Around line 53-56: The tests leak SWR state because mockSWRData isn't cleared
between runs; update the test suite's beforeEach (the existing beforeEach that
calls jest.clearAllMocks() and mockUsePathname.mockReturnValue('/')) to also
reset or reinitialize mockSWRData (e.g., set it to an empty object or call its
reset method) so each test starts with a clean SWR mock state; reference
mockSWRData and the beforeEach block when making the change.

---

Nitpick comments:
In `@web-ui/__tests__/components/sessions/SessionListView.test.tsx`:
- Around line 142-153: The test shows the Retry button but doesn't assert its
behavior; update the 'shows error state with retry button' test to simulate a
user click on the Retry button and assert that the mocked mutate function is
called. Keep using the existing mockUseSWR return value (with const mutate =
jest.fn()) and SessionListView render, then use userEvent.click or
fireEvent.click on screen.getByRole('button', { name: /retry/i }) and
expect(mutate).toHaveBeenCalled();.

In `@web-ui/src/components/sessions/SessionListView.tsx`:
- Line 158: Replace the raw HTML <button> used for the dismiss action in
SessionListView.tsx with the Shadcn Button component (e.g., Button from your
shadcn/ui bundle) and wire the same onClick handler (setEndError(null)); remove
the underline class and instead use the appropriate Button variant/props (e.g.,
a link/ghost style or gray color scheme) plus the ml-2 spacing so visual layout
is preserved, and add the Button import to the top of the file if missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e21ba614-a186-45d5-8580-27d3cc930a47

📥 Commits

Reviewing files that changed from the base of the PR and between be2fd2b and a971be4.

📒 Files selected for processing (11)
  • web-ui/__tests__/components/layout/AppSidebar.test.tsx
  • web-ui/__tests__/components/sessions/NewSessionModal.test.tsx
  • web-ui/__tests__/components/sessions/SessionCard.test.tsx
  • web-ui/__tests__/components/sessions/SessionListView.test.tsx
  • web-ui/src/app/sessions/page.tsx
  • web-ui/src/components/layout/AppSidebar.tsx
  • web-ui/src/components/sessions/NewSessionModal.tsx
  • web-ui/src/components/sessions/SessionCard.tsx
  • web-ui/src/components/sessions/SessionListView.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts

Comment on lines +126 to +129
{label === 'Sessions' && activeSessionCount > 0 && (
<span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-blue-500 px-1.5 text-[10px] font-bold text-white">
{activeSessionCount}
</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use gray theme tokens for the Sessions badge instead of hardcoded blue.

Line 127 introduces bg-blue-500, which diverges from the UI gray color scheme requirement in this codebase.

🎨 Suggested fix
-                <span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-blue-500 px-1.5 text-[10px] font-bold text-white">
+                <span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-muted px-1.5 text-[10px] font-bold text-foreground">
                   {activeSessionCount}
                 </span>

As per coding guidelines web-ui/src/**/*.{ts,tsx}: Web UI must use Shadcn/UI (Nova preset) with gray color scheme and Hugeicons (@hugeicons/react); never use lucide-react.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{label === 'Sessions' && activeSessionCount > 0 && (
<span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-blue-500 px-1.5 text-[10px] font-bold text-white">
{activeSessionCount}
</span>
{label === 'Sessions' && activeSessionCount > 0 && (
<span className="ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-muted px-1.5 text-[10px] font-bold text-foreground">
{activeSessionCount}
</span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/layout/AppSidebar.tsx` around lines 126 - 129, The
Sessions badge currently hardcodes a blue token ("bg-blue-500") in the JSX where
label === 'Sessions' and activeSessionCount is rendered; update the className
for the badge (the span that renders {activeSessionCount}) to use the project's
Shadcn/UI Nova gray theme tokens instead of bg-blue-500 (e.g., replace blue
token with the corresponding gray background and matching gray/white text tokens
from the Nova preset such as bg-gray-500/600 and text-gray-900 or text-white as
appropriate) so it follows the gray color scheme used across web-ui components.

open: boolean;
onOpenChange: (open: boolean) => void;
defaultWorkspacePath: string;
onSubmit: (data: { workspace_path: string; model: string }) => Promise<void>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract session-create payload type to shared types.

{ workspace_path: string; model: string } should be a shared exported type from web-ui/src/types/index.ts and imported here (and in SessionListView) to satisfy repo typing rules and avoid drift.

💡 Suggested refactor
- onSubmit: (data: { workspace_path: string; model: string }) => Promise<void>;
+ onSubmit: (data: SessionCreateRequest) => Promise<void>;
// web-ui/src/types/index.ts
export interface SessionCreateRequest {
  workspace_path: string;
  model: string;
}
+ import type { SessionCreateRequest } from '@/types';

As per coding guidelines: "TypeScript types must be defined in web-ui/src/types/index.ts and API client functionality in web-ui/src/lib/api.ts".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/sessions/NewSessionModal.tsx` at line 33, Extract the
inline payload type used by NewSessionModal's onSubmit ((data: { workspace_path:
string; model: string }) => Promise<void>) into a shared exported interface
named SessionCreateRequest in web-ui/src/types/index.ts, export it, then import
and use that type in NewSessionModal (replace the inline shape on onSubmit) and
in SessionListView so both components consume the same SessionCreateRequest
type; update any affected function signatures (e.g., onSubmit) and imports
accordingly.

Comment thread web-ui/src/components/sessions/SessionCard.tsx Outdated
Comment thread web-ui/src/components/sessions/SessionListView.tsx
const session = await sessionsApi.create(createData);
setModalOpen(false);
mutate();
router.push(`/sessions/${session.id}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether a dynamic sessions detail route exists.
fd -a -p 'page.tsx' web-ui/src/app/sessions | sed 's#^\./##'

Repository: frankbria/codeframe

Length of output: 114


Add the missing dynamic session detail route.

Line 64 calls router.push(\/sessions/${session.id}`), but the dynamic page web-ui/src/app/sessions/[id]/page.tsxdoes not exist. Only the static/sessions` page is present. This breaks all navigation to session details (create, resume, view). Create the missing dynamic route file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/sessions/SessionListView.tsx` at line 64, Add the
missing dynamic session detail route for the /sessions/:id path by creating a
route component that accepts the route parameter id (e.g., export default async
function SessionPage({ params }) { ... }) and fetches the session by params.id,
rendering the session details (or a not-found state) so
router.push(`/sessions/${session.id}`) resolves; ensure the component handles
create/resume/view flows, returns appropriate JSX/ReactNode, and exports
metadata or error handling as needed to match existing sessions list behavior.

Comment thread web-ui/src/lib/api.ts
Comment on lines +634 to +645
getAll: async (
workspacePath: string,
params?: { state?: SessionState }
): Promise<SessionListResponse> => {
const response = await api.get<SessionListResponse>('/api/v2/sessions', {
params: {
workspace_path: workspacePath,
...(params?.state ? { state: params.state } : {}),
},
});
return response.data;
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

sessionsApi.getAll response type does not match backend contract.

Line 638 currently assumes { sessions, total }, but backend GET /api/v2/sessions is defined as a list response. This will cause sessionData.sessions to be undefined at runtime.

🔧 Suggested fix
-  ): Promise<SessionListResponse> => {
-    const response = await api.get<SessionListResponse>('/api/v2/sessions', {
+  ): Promise<SessionListResponse> => {
+    const response = await api.get<Session[]>('/api/v2/sessions', {
       params: {
         workspace_path: workspacePath,
         ...(params?.state ? { state: params.state } : {}),
       },
     });
-    return response.data;
+    return {
+      sessions: response.data,
+      total: response.data.length,
+    };
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/lib/api.ts` around lines 634 - 645, sessionsApi.getAll currently
assumes a { sessions, total } shape but the backend returns a list response;
update getAll and its types so the returned value matches the backend contract:
adjust the SessionListResponse type to the actual list response shape returned
by GET /api/v2/sessions (e.g., an array or ListResponse<T> as used elsewhere)
and return response.data exactly (or map response.data.results to the expected
shape) from getAll instead of fabricating { sessions, total }; update any
callers expecting sessionData.sessions to use the corrected property name (e.g.,
the array itself or .results) so runtime undefined errors are resolved.

- Add SessionCreateRequest type to src/types/index.ts
- Fix sessionsApi.getAll: backend returns Session[] array, wrap as { sessions, total }
- SessionCard: Button asChild > Link (fix interactive element nesting a11y)
- SessionCard: format cost_usd with .toFixed(4) to avoid float representation issues
- NewSessionModal: use SessionCreateRequest type; reset isSubmitting on open
- SessionListView: clear endError at start of handleEnd before retry
- AppSidebar: Sessions badge uses bg-muted/text-foreground (gray theme, not blue)
- AppSidebar: align SWR cache key with actual API endpoint
- Create sessions/[id]/page.tsx (missing dynamic route — broke all detail navigation)
- Fix AppSidebar tests: reset mockSWRData in beforeEach, update SWR key + badge class
@claude
Copy link
Copy Markdown

claude Bot commented Apr 2, 2026

PR Review — Sessions List Page + Sidebar Navigation (#508)

This is a solid, well-structured piece of UI work. TDD approach with 112 tests, clear component decomposition, proper SWR patterns, and good handling of the hydration guard — the fundamentals are right. A few issues worth addressing before merging:


Issues

1. NewSessionModal — empty workspace path can be submitted

handleSubmit fires with no validation. A user can clear the input and submit an empty string, which will hit the API with workspace_path: ''.

const handleSubmit = async () => {
  setIsSubmitting(true);
  try {
    await onSubmit({ workspace_path: workspacePath, model });

Add a guard: if (!workspacePath.trim()) return; before setIsSubmitting(true), and disable the Start Session button when the field is empty.


2. SessionListView.handleCreate — creation errors are unhandled

handleCreate is async but has no try/catch. If sessionsApi.create() rejects, the error propagates up to NewSessionModal.handleSubmit, which has a finally that resets the spinner — but the user sees nothing. The modal just silently stops submitting.

const handleCreate = useCallback(async (createData: SessionCreateRequest) => {
  const session = await sessionsApi.create(createData);   // ← unhandled rejection
  setModalOpen(false);
  mutate();
  router.push(`/sessions/${session.id}`);
}, [mutate, router]);

Wrap in try/catch and surface the error the same way handleEnd does — set endError (or a separate createError state) and render an inline banner. The existing endError pattern is a good template.


3. AppSidebar — badge dispatch via string comparison is fragile

{label === 'Sessions' && activeSessionCount > 0 && (
{label === 'Blockers' && openBlockerCount > 0 && (

This will silently break if the label text ever changes. Consider adding an optional badge?: number field to NavItem and populating it before rendering:

const navItemsWithBadges = NAV_ITEMS.map((item) => ({
  ...item,
  badge: item.href === '/sessions' ? activeSessionCount
       : item.href === '/blockers' ? openBlockerCount
       : 0,
}));

Then the render loop becomes {item.badge > 0 && <BadgeSpan>{item.badge}</BadgeSpan>} — no string matching.


4. sessions/[id]/page.tsx — placeholder leads to a dead end

The detail page is clearly a stub, but SessionCard links Resume/View directly to /sessions/[id]. Users who click Resume land on "Session detail view coming soon." This is surprising. Consider either:

  • Disabling Resume/View links until the detail page exists (with a tooltip explaining why), or
  • Adding a note in the PR that this is an intentional placeholder with a follow-up issue.

If there's a tracking issue for the detail page, linking it here would help.


5. SWR cache key vs actual API parameter naming

The SWR key in AppSidebar uses path=:

/api/v2/sessions?path=<encoded>&state=active

But the actual API call in sessionsApi.getAll sends workspace_path=:

params: { workspace_path: workspacePath, ... }

The SWR key is just an identifier so this isn't a bug, but it creates a confusing mismatch when debugging (e.g., comparing DevTools network requests vs SWR cache). Consider aligning the key to match the actual query string.


Minor

  • window.confirm in SessionCard — works fine for now, but worth tracking for accessibility and mobile parity. A shadcn AlertDialog (already in the component library based on the imports) would be the idiomatic upgrade path when the time comes.
  • sessionsApi.getAll wraps response: total: response.data.length discards any server-side pagination total. Not an issue today, but if the backend ever returns paginated results, the client total will be wrong. Worth a comment noting this assumption.

What's working well

  • Hydration guard pattern (workspaceReady state) is clean and consistent with the existing page pattern.
  • handleEnd always-mutate pattern (re-syncs SWR even on error) is correct.
  • useEffect reset in NewSessionModal on each open prevents stale state.
  • SWR key encoding with encodeURIComponent is correct.
  • The 30s vs 10s polling split (sidebar badge vs list) is a sensible tradeoff.
  • Test fixtures (makeSession helper) and mock isolation (beforeEach clearing mockSWRData) are well done.

Items 1 and 2 are the most user-visible gaps. Item 3 is a maintainability improvement worth making while the pattern is fresh.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
web-ui/src/components/sessions/SessionListView.tsx (1)

26-27: Align the SWR key with the actual API query param (workspace_path).

At Line 26, using path in the key while the request uses workspace_path can create cache-key drift and make shared invalidation harder across components.

Suggested diff
-    `/api/v2/sessions?path=${encodeURIComponent(workspacePath)}`,
+    `/api/v2/sessions?workspace_path=${encodeURIComponent(workspacePath)}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/sessions/SessionListView.tsx` around lines 26 - 27, The
SWR key string in SessionListView.tsx uses `path` while the actual request and
backend expect `workspace_path`, causing cache-key drift; update the key used
with sessionsApi.getAll to
`/api/v2/sessions?workspace_path=${encodeURIComponent(workspacePath)}` (or
switch to a tuple key like ['/api/v2/sessions', workspacePath]) so it matches
the API query param and consistent invalidation across components; check usages
around sessionsApi.getAll and other components to ensure they use the same
`workspace_path` key format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/src/components/sessions/SessionListView.tsx`:
- Around line 157-160: Replace the raw HTML <button> in SessionListView that
dismisses endError with the Shadcn Button component used across the app: import
the Button from the shared UI components and change the element that calls
setEndError(null) to use <Button ... onClick={() =>
setEndError(null)}>Dismiss</Button> (choose the appropriate variant/size to
match the current styling, e.g., variant="link" or size="sm"); ensure the Button
import is added to the top of the SessionListView file and preserve
accessibility (aria-label) if needed and any existing className styling.

---

Nitpick comments:
In `@web-ui/src/components/sessions/SessionListView.tsx`:
- Around line 26-27: The SWR key string in SessionListView.tsx uses `path` while
the actual request and backend expect `workspace_path`, causing cache-key drift;
update the key used with sessionsApi.getAll to
`/api/v2/sessions?workspace_path=${encodeURIComponent(workspacePath)}` (or
switch to a tuple key like ['/api/v2/sessions', workspacePath]) so it matches
the API query param and consistent invalidation across components; check usages
around sessionsApi.getAll and other components to ensure they use the same
`workspace_path` key format.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7765890c-4a44-4fe7-bff7-406e0325806a

📥 Commits

Reviewing files that changed from the base of the PR and between a971be4 and 470300d.

📒 Files selected for processing (9)
  • web-ui/__tests__/components/layout/AppSidebar.test.tsx
  • web-ui/__tests__/components/sessions/SessionCard.test.tsx
  • web-ui/src/app/sessions/[id]/page.tsx
  • web-ui/src/components/layout/AppSidebar.tsx
  • web-ui/src/components/sessions/NewSessionModal.tsx
  • web-ui/src/components/sessions/SessionCard.tsx
  • web-ui/src/components/sessions/SessionListView.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/types/index.ts
✅ Files skipped from review due to trivial changes (2)
  • web-ui/src/components/sessions/SessionCard.tsx
  • web-ui/tests/components/sessions/SessionCard.test.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • web-ui/src/components/layout/AppSidebar.tsx
  • web-ui/src/lib/api.ts
  • web-ui/src/components/sessions/NewSessionModal.tsx
  • web-ui/tests/components/layout/AppSidebar.test.tsx
  • web-ui/src/types/index.ts

Comment on lines +157 to +160
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
{endError}
<button className="ml-2 underline" onClick={() => setEndError(null)}>Dismiss</button>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use Shadcn Button instead of raw <button> for dismiss action.

At Line 159, this bypasses the shared UI system used elsewhere in the file.

Suggested diff
-          <button className="ml-2 underline" onClick={() => setEndError(null)}>Dismiss</button>
+          <Button
+            variant="link"
+            size="sm"
+            className="ml-2 h-auto p-0 text-destructive underline"
+            onClick={() => setEndError(null)}
+          >
+            Dismiss
+          </Button>

As per coding guidelines, web-ui/src/**/*.{ts,tsx} must use Shadcn/UI (Nova preset) and Hugeicons in Web UI components.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
{endError}
<button className="ml-2 underline" onClick={() => setEndError(null)}>Dismiss</button>
</div>
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
{endError}
<Button
variant="link"
size="sm"
className="ml-2 h-auto p-0 text-destructive underline"
onClick={() => setEndError(null)}
>
Dismiss
</Button>
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/sessions/SessionListView.tsx` around lines 157 - 160,
Replace the raw HTML <button> in SessionListView that dismisses endError with
the Shadcn Button component used across the app: import the Button from the
shared UI components and change the element that calls setEndError(null) to use
<Button ... onClick={() => setEndError(null)}>Dismiss</Button> (choose the
appropriate variant/size to match the current styling, e.g., variant="link" or
size="sm"); ensure the Button import is added to the top of the SessionListView
file and preserve accessibility (aria-label) if needed and any existing
className styling.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 2, 2026

Code Review: Sessions List Page + Sidebar Navigation (#508)

Good overall implementation — the component structure is clean, the test coverage is thorough, and the workspace hydration guard pattern is followed correctly. A few things worth addressing:


Issues

1. Wrong icon for empty state (SessionListView.tsx)

<Loading03Icon className="mx-auto mb-3 h-10 w-10 text-muted-foreground" />
<p className="text-sm font-medium text-foreground">No sessions yet</p>

Loading03Icon is a spinner/activity icon — semantically wrong for an empty state. Use a neutral icon like InboxIcon, CommandLineIcon, or similar. This will also confuse screen readers.


2. Fragile label-string badge dispatch in AppSidebar.tsx

{label === 'Sessions' && activeSessionCount > 0 && (
{label === 'Blockers' && openBlockerCount > 0 && (

This ties badge rendering to the human-readable label string. If either label is renamed, the badge silently disappears with no compile-time error. The NavItem interface already exists — add an optional badge field or a typed id field to avoid the string coupling:

interface NavItem {
  href: string;
  label: string;
  id?: 'sessions' | 'blockers';  // or badge data
  ...
}

3. Silent failure on session creation error (NewSessionModal.tsx + SessionListView.tsx)

NewSessionModal.handleSubmit calls onSubmit and only wraps it in finally to reset isSubmitting. If sessionsApi.create throws, the modal stays open and the button re-enables — but no error is shown to the user.

handleCreate in SessionListView also has no try/catch, so the error just propagates silently.

Consider adding an error state to NewSessionModal (similar to how endError is handled in SessionListView) and passing it up or handling it in handleCreate.


4. window.confirm in SessionCard.tsx

if (window.confirm('End this session? This will stop the active agent.')) {

window.confirm is a browser-blocking call that looks inconsistent with the rest of the UI (which uses proper Dialog/Modal components). It also breaks in some test environments (you're already mocking it). A small inline confirmation or a AlertDialog component would be more cohesive. This matches the same approach used elsewhere for destructive actions.


5. sessionsApi.getAll response shape assumption (api.ts)

const response = await api.get<Session[]>('/api/v2/sessions', { ... });
return { sessions: response.data, total: response.data.length };

This assumes the backend returns a flat Session[]. If the backend endpoint is consistent with the rest of the v2 API (which returns typed response objects), this may need to be api.get<SessionListResponse>. The total field derived from response.data.length would also miss server-side pagination totals if the API ever supports that. Worth verifying against the actual sessions_v2 router before this merges.


Minor

  • The session detail page placeholder is fine as a scaffold — just make sure Frontend: /sessions list page + sidebar navigation entry #508's acceptance criteria tracks that it remains a stub until the detail view PR lands.
  • The SWR key in AppSidebar uses path= while sessionsApi.getAll sends workspace_path= as the actual HTTP param. The SWR key is only a cache identifier so this isn't a runtime bug, but it's inconsistent and could confuse future readers.
  • MODEL_OPTIONS in NewSessionModal hardcodes claude-haiku-4-5 while the current model IDs elsewhere (e.g. SessionCard.test.tsx) use claude-haiku-4-5. This is fine, but worth keeping in sync with CLAUDE.md's model table as versions update.

Tests

112 tests is solid coverage. The SWR mock reset in beforeEach (Object.keys(mockSWRData).forEach((k) => delete mockSWRData[k])) is a good pattern to prevent cross-test leakage — nice fix.

The window.confirm mock approach in SessionCard.test.tsx works but is a signal that the component itself should use a proper dialog instead (see issue #4 above).

@frankbria frankbria merged commit b2dff53 into main Apr 2, 2026
10 checks passed
@frankbria frankbria deleted the feature/issue-508-sessions-list-page branch April 2, 2026 02:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Frontend: /sessions list page + sidebar navigation entry

1 participant