feat: add Sessions Hub page

- Add Sessions tab to navigation between Dashboard and Projects
- Show active session count badge in navigation
- Create SessionsPage with:
  - Last session section with resume button
  - Active sessions grid with open/stop actions
  - Recent sessions list
  - Create session form with project/repo/tool selectors
- Add last_session_id to user config
- Update UserConfig schemas (backend and frontend)
- Add comprehensive CSS for sessions page

Quality gates: typecheck ✓, lint ✓, build ✓
This commit is contained in:
Fusion
2026-05-19 23:06:54 +02:00
parent 4f695d7e62
commit 94aa88c154
17 changed files with 996 additions and 11 deletions
+2
View File
@@ -47,6 +47,7 @@ class UserConfigResponse(BaseModel):
theme: str = "system"
git_user_name: str | None = None
git_user_email: str | None = None
last_session_id: str | None = None
class UserConfigUpdate(BaseModel):
@@ -54,6 +55,7 @@ class UserConfigUpdate(BaseModel):
theme: str | None = None
git_user_name: str | None = None
git_user_email: str | None = None
last_session_id: str | None = None
@router.get(
+2
View File
@@ -5,6 +5,7 @@ export interface UserConfig {
theme: string;
git_user_name: string | null;
git_user_email: string | null;
last_session_id: string | null;
}
export interface UserConfigUpdate {
@@ -12,6 +13,7 @@ export interface UserConfigUpdate {
theme?: string;
git_user_name?: string;
git_user_email?: string;
last_session_id?: string;
}
export const getUserConfig = async (): Promise<UserConfig> => {
+19 -11
View File
@@ -11,6 +11,7 @@ import type { IconName } from "../utils/icons";
const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
{ to: "/", label: "Dashboard", icon: "dashboard" },
{ to: "/sessions", label: "Sessions", icon: "terminal" },
{ to: "/projects", label: "Projects", icon: "projects" },
{ to: "/ssh-keys", label: "SSH Keys", icon: "profile" },
{ to: "/tool-types", label: "Tool Types", icon: "code" },
@@ -83,17 +84,24 @@ export const AppShell = () => {
<div className="shell-body">
<aside className="shell-nav" aria-label="Primary navigation">
{NAV_ITEMS.map((item) => (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
end={item.to === "/"}
>
<Icon name={item.icon} size="sm" />
{item.label}
</NavLink>
))}
{NAV_ITEMS.map((item) => {
const isSessions = item.to === "/sessions";
const activeCount = sessions.filter((s) => s.status === "running").length;
return (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")}
end={item.to === "/"}
>
<Icon name={item.icon} size="sm" />
{item.label}
{isSessions && activeCount > 0 && (
<span className="nav-badge">{activeCount}</span>
)}
</NavLink>
);
})}
{sessions.length > 0 && (
<>
+416
View File
@@ -0,0 +1,416 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { listProjects } from "../api/projects";
import type { Project } from "../types";
import { listRepositories, type GitRepository } from "../api/git_repositories";
import { getUserSessions, type Session, deleteInstance, stopInstance } from "../api/sessions";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { createInstance } from "../api/sessions";
import { getUserConfig, updateUserConfig } from "../api/settings";
import { Icon } from "../components/icon";
type SessionsStatus = "loading" | "ready" | "error";
type CreateStatus = "idle" | "creating" | "error";
export const SessionsPage = () => {
const navigate = useNavigate();
const [status, setStatus] = useState<SessionsStatus>("loading");
const [sessions, setSessions] = useState<Session[]>([]);
const [lastSessionId, setLastSessionId] = useState<string | null>(null);
const [projects, setProjects] = useState<Project[]>([]);
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [selectedProject, setSelectedProject] = useState<string>("");
const [selectedRepo, setSelectedRepo] = useState<string>("");
const [selectedToolType, setSelectedToolType] = useState<string>("");
const [displayName, setDisplayName] = useState("");
const [createStatus, setCreateStatus] = useState<CreateStatus>("idle");
const [createError, setCreateError] = useState<string | null>(null);
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const loadSessions = useCallback(async () => {
setStatus("loading");
try {
const [sessionsData, config] = await Promise.all([
getUserSessions(),
getUserConfig(),
]);
setSessions(sessionsData);
setLastSessionId(config.last_session_id ?? null);
setStatus("ready");
} catch {
setStatus("error");
}
}, []);
useEffect(() => {
void loadSessions();
}, [loadSessions]);
useEffect(() => {
const loadProjects = async () => {
try {
const data = await listProjects();
setProjects(data);
} catch {
// ignore
}
};
void loadProjects();
}, []);
useEffect(() => {
const loadToolTypes = async () => {
try {
const data = await listToolTypes();
setToolTypes(data);
} catch {
// ignore
}
};
void loadToolTypes();
}, []);
useEffect(() => {
if (!selectedProject) {
setRepositories([]);
return;
}
const loadRepos = async () => {
try {
const data = await listRepositories(selectedProject);
setRepositories(data);
} catch {
setRepositories([]);
}
};
void loadRepos();
}, [selectedProject]);
const activeSessions = useMemo(
() => sessions.filter((s) => s.status === "running"),
[sessions]
);
const recentSessions = useMemo(
() => sessions.filter((s) => s.status !== "running").slice(0, 5),
[sessions]
);
const lastSession = useMemo(
() => sessions.find((s) => s.id === lastSessionId) ?? null,
[sessions, lastSessionId]
);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setCreateError(null);
if (!selectedProject || !selectedRepo || !selectedToolType) {
setCreateError("Project, repository, and tool type are required");
return;
}
setCreateStatus("creating");
try {
const instance = await createInstance(
selectedProject,
selectedRepo,
selectedToolType,
displayName || undefined
);
await updateUserConfig({ last_session_id: instance.id });
setCreateStatus("idle");
setSelectedProject("");
setSelectedRepo("");
setSelectedToolType("");
setDisplayName("");
await loadSessions();
} catch {
setCreateStatus("error");
setCreateError("Failed to create session");
}
};
const handleStop = async (sessionId: string, projectId: string, repoId: string) => {
try {
await stopInstance(projectId, repoId, sessionId);
await loadSessions();
} catch {
// ignore
}
};
const handleDelete = async (sessionId: string, projectId: string, repoId: string) => {
try {
await deleteInstance(projectId, repoId, sessionId);
setDeleteConfirmId(null);
await loadSessions();
} catch {
setDeleteConfirmId(null);
}
};
const handleOpen = (session: Session) => {
navigate(`/projects/${session.project_name}/repositories/${session.repository_name}`);
};
const handleResumeLast = async () => {
if (!lastSession) return;
// Find the project and repo IDs
const project = projects.find((p) => p.name === lastSession.project_name);
if (project) {
navigate(`/projects/${project.id}`);
}
};
return (
<section className="stack sessions-page">
<div className="page-header">
<h1>Sessions</h1>
</div>
{status === "loading" && <p className="muted">Loading sessions...</p>}
{status === "error" && (
<div className="card stack">
<p>Failed to load sessions</p>
<button className="secondary-button" onClick={() => void loadSessions()} type="button">
<Icon name="refresh" size="sm" />
Retry
</button>
</div>
)}
{status === "ready" && (
<>
{/* Last Session */}
{lastSession && (
<div className="last-session-section">
<h2>Last Session</h2>
<div className="card last-session-card">
<div className="last-session-info">
<h3>{lastSession.display_name}</h3>
<p className="muted">
{lastSession.tool_type_name} · {lastSession.project_name} · {lastSession.repository_name}
</p>
<span className={`status-badge ${lastSession.status}`}>{lastSession.status}</span>
</div>
<div className="last-session-actions">
<button className="primary-button" onClick={handleResumeLast} type="button">
<Icon name="play" size="sm" />
Resume
</button>
</div>
</div>
</div>
)}
{/* Active Sessions */}
<div className="active-sessions-section">
<h2>
Active Sessions
{activeSessions.length > 0 && (
<span className="badge">{activeSessions.length}</span>
)}
</h2>
{activeSessions.length === 0 ? (
<p className="muted">No active sessions</p>
) : (
<div className="sessions-grid">
{activeSessions.map((session) => (
<div className="card session-card" key={session.id}>
<div className="session-info">
<h4>{session.display_name}</h4>
<p className="muted">
{session.tool_type_name} · {session.project_name}
</p>
<span className="status-badge running">running</span>
</div>
<div className="session-actions">
<button
className="secondary-button small"
onClick={() => handleOpen(session)}
type="button"
>
<Icon name="external" size="sm" />
Open
</button>
<button
className="secondary-button small"
onClick={() =>
void handleStop(
session.id,
projects.find((p) => p.name === session.project_name)?.id ?? "",
""
)
}
type="button"
>
<Icon name="stop" size="sm" />
Stop
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Recent Sessions */}
{recentSessions.length > 0 && (
<div className="recent-sessions-section">
<h2>Recent Sessions</h2>
<div className="recent-sessions-list">
{recentSessions.map((session) => (
<div className="recent-session-item" key={session.id}>
<div className="recent-session-info">
<span className="recent-session-name">{session.display_name}</span>
<span className="muted">
{session.tool_type_name} · {session.project_name}
</span>
</div>
<div className="recent-session-actions">
<button
className="ghost-button small"
onClick={() => handleOpen(session)}
type="button"
>
Open
</button>
{deleteConfirmId === session.id ? (
<div className="delete-confirm-inline">
<button
className="danger-button small"
onClick={() =>
void handleDelete(
session.id,
projects.find((p) => p.name === session.project_name)?.id ?? "",
""
)
}
type="button"
>
Delete
</button>
<button
className="ghost-button small"
onClick={() => setDeleteConfirmId(null)}
type="button"
>
Cancel
</button>
</div>
) : (
<button
className="ghost-button small danger-text"
onClick={() => setDeleteConfirmId(session.id)}
type="button"
>
<Icon name="delete" size="sm" />
</button>
)}
</div>
</div>
))}
</div>
</div>
)}
{/* Create Session */}
<div className="create-session-section">
<h2>Create New Session</h2>
<form onSubmit={handleCreate} className="card stack create-session-form">
<div className="form-row">
<label className="form-field">
Project
<select
value={selectedProject}
onChange={(e) => {
setSelectedProject(e.target.value);
setSelectedRepo("");
}}
>
<option value="">Select project...</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</label>
<label className="form-field">
Repository
<select
value={selectedRepo}
onChange={(e) => setSelectedRepo(e.target.value)}
disabled={!selectedProject}
>
<option value="">Select repository...</option>
{repositories.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
</select>
</label>
<label className="form-field">
Tool Type
<select
value={selectedToolType}
onChange={(e) => setSelectedToolType(e.target.value)}
>
<option value="">Select tool...</option>
{toolTypes.map((t) => (
<option key={t.id} value={t.id}>
{t.display_name}
</option>
))}
</select>
</label>
</div>
<label className="form-field">
Display Name (optional)
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="My Development Environment"
/>
</label>
{createError && <p className="error-text">{createError}</p>}
<div className="form-actions">
<button
className="primary-button"
type="submit"
disabled={createStatus === "creating"}
>
{createStatus === "creating" ? (
<>
<Icon name="loading" size="sm" />
Creating...
</>
) : (
<>
<Icon name="add" size="sm" />
Create Session
</>
)}
</button>
</div>
</form>
</div>
</>
)}
</section>
);
};
+1
View File
@@ -18,6 +18,7 @@ export const SettingsPage = () => {
default_editor: null,
git_user_name: null,
git_user_email: null,
last_session_id: null,
});
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
+2
View File
@@ -4,6 +4,7 @@ import { AppShell } from "./components/app-shell";
import { ProtectedRoute } from "./components/protected-route";
import { DashboardPage } from "./pages/dashboard";
import { LoginRedirectPage, NotFoundPage } from "./pages/placeholder";
import { SessionsPage } from "./pages/sessions";
import { ProfilePage } from "./pages/profile";
import { ProjectsPage } from "./pages/projects";
import { GitRepositoriesPage } from "./pages/git-repositories";
@@ -28,6 +29,7 @@ export const AppRouter = () => {
}
>
<Route index element={<DashboardPage />} />
<Route path="sessions" element={<SessionsPage />} />
<Route path="projects" element={<ProjectsPage />} />
<Route path="projects/:projectId" element={<RepoWorkspace />} />
<Route path="projects/:projectId/repositories" element={<GitRepositoriesPage />} />
+173
View File
@@ -2456,3 +2456,176 @@ a.nav-item,
font-size: 1.25rem;
}
}
/* ============================================
Sessions Page Styles
============================================ */
.nav-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: var(--primary);
color: var(--primary-fg);
border-radius: 9px;
font-size: 11px;
font-weight: 600;
margin-left: auto;
}
.sessions-page {
max-width: 1200px;
}
.last-session-section {
margin-bottom: var(--space-6);
}
.last-session-card {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
padding: var(--space-5);
border: 2px solid var(--primary);
}
.last-session-info h3 {
margin: 0 0 var(--space-1) 0;
font-size: 1.25rem;
}
.active-sessions-section {
margin-bottom: var(--space-6);
}
.active-sessions-section h2 {
display: flex;
align-items: center;
gap: var(--space-2);
}
.active-sessions-section .badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 6px;
background: var(--success);
color: white;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.sessions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-4);
}
.session-card {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-4);
}
.session-info h4 {
margin: 0 0 var(--space-1) 0;
font-size: 1rem;
}
.session-actions {
display: flex;
gap: var(--space-2);
}
.recent-sessions-section {
margin-bottom: var(--space-6);
}
.recent-sessions-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.recent-session-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) var(--space-4);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
}
.recent-session-info {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.recent-session-name {
font-weight: 500;
}
.recent-session-actions {
display: flex;
gap: var(--space-2);
align-items: center;
}
.delete-confirm-inline {
display: flex;
gap: var(--space-2);
}
.create-session-section {
margin-bottom: var(--space-6);
}
.create-session-form {
max-width: 600px;
}
.create-session-form .form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-4);
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
text-transform: capitalize;
}
.status-badge.running {
background: var(--success-light, #dcfce7);
color: var(--success, #16a34a);
}
.status-badge.stopped {
background: var(--muted-bg, #f3f4f6);
color: var(--muted, #6b7280);
}
.status-badge.pending {
background: var(--warning-light, #fef3c7);
color: var(--warning, #d97706);
}
.status-badge.error {
background: var(--danger-light, #fee2e2);
color: var(--danger, #dc2626);
}
@@ -0,0 +1,2 @@
schema: spec-driven
name: sessions-hub
+120
View File
@@ -0,0 +1,120 @@
# Sessions Hub - Design
## Architecture
```
Sessions Hub
├── Navigation
│ └── "Sessions" tab (between Dashboard and Projects)
│ └── Badge with active session count
├── SessionsPage
│ ├── Last Session Section
│ │ └── Quick access card with resume button
│ ├── Active Sessions Section
│ │ └── Grid of active session cards
│ ├── Recent Sessions Section
│ │ └── List of recent sessions
│ └── Create Session Section
│ └── Project selector + tool type selector
└── User Config
└── last_session_id field
```
## Component Design
### SessionsPage
**Sections:**
1. **Last Session** (if exists)
- Large card showing last session details
- "Resume" button to open the workspace
- Shows project, repository, tool type
2. **Active Sessions**
- Grid of cards for running instances
- Each card: name, type, status badge, action buttons
- Actions: Open, Stop, Restart, Delete
3. **Recent Sessions**
- List of last 5 sessions (any status)
- Compact list view with status indicators
- Click to navigate to workspace
4. **Create New Session**
- Project dropdown (all user's projects)
- Repository dropdown (filtered by project)
- Tool type dropdown
- Display name input
- "Create" button
### AppShell Updates
**Navigation:**
```
Dashboard | Sessions (3) | Projects | SSH Keys | Tool Types | Settings
```
**Badge:**
- Shows count of active (running) sessions
- Updates via existing sessions polling
### User Config Extension
**New field:**
```typescript
interface UserConfig {
// existing fields...
last_session_id: string | null;
}
```
**Update timing:**
- Set when creating a new session
- Set when opening/resuming a session
## Data Flow
### Loading Sessions Page
1. Fetch user config (for last_session_id)
2. Fetch all user sessions via `/users/me/sessions`
3. Filter into active vs recent
4. Display last session if available
### Creating Session
1. User selects project, repo, tool type
2. POST to `/projects/{id}/repositories/{id}/instances`
3. On success: update user config with last_session_id
4. Refresh sessions list
### Resuming Session
1. User clicks "Resume" on last session
2. Navigate to workspace with session active
3. Update user config (reinforce as last)
## API Changes
### GET /users/me/sessions
Already exists - returns all sessions for user.
### PATCH /users/me/config
Already exists - add `last_session_id` to config schema.
## Technical Details
**Frontend:**
- New page: `pages/sessions.tsx`
- Update: `app-shell.tsx` for navigation
- Update: `api/settings.ts` for config type
- Update: `state/sessions.tsx` for badge count
**Backend:**
- Update: `models/user_config.py` schema
- Update: `api/user_config.py` to accept last_session_id
**No new backend endpoints needed** - reuse existing APIs.
## Error Handling
- No sessions: Show empty state with "Create your first session" CTA
- Failed to load: Show error with retry button
- Create failed: Show error message, keep form open
+51
View File
@@ -0,0 +1,51 @@
# Sessions Hub
## Problem
Users currently have to navigate into individual projects and repositories to see their active tool instances (sessions). There's no centralized place to:
- See all active/open sessions at a glance
- Quickly access the last used session
- Create new sessions without navigating deep into the project hierarchy
## Solution
Create a dedicated **Sessions Hub** page that serves as the central place for managing tool instances:
1. **Navigation tab** between Dashboard and Projects
2. **Active sessions section** showing all running/open instances
3. **Last session** prominently displayed for quick access
4. **Quick create** - create sessions for any project from one place
5. **Persist last session** in user config for easier access
## Key Features
### Sessions Page
- Shows all active (running) sessions with status, type, and links
- Shows recent sessions (last 5)
- Shows last created session at the top for quick access
- "New Session" button to create instances for any project
### Navigation
- New "Sessions" tab in the app shell between Dashboard and Projects
- Shows count of active sessions as a badge
### Quick Access
- Last created session saved to user config
- One-click to reopen/resume last session
- Session history for quick navigation
## Benefits
- **Faster workflow** - No need to navigate deep into projects
- **Better visibility** - See all active work at a glance
- **Quick resume** - Jump back to last work instantly
- **Centralized management** - One place for all sessions
## Success Criteria
- [ ] Sessions tab visible in navigation between Dashboard and Projects
- [ ] Sessions page shows active sessions
- [ ] Last session displayed prominently
- [ ] Can create session for any project from Sessions page
- [ ] Last created session persists in user config
- [ ] Badge shows count of active sessions
+115
View File
@@ -0,0 +1,115 @@
# Sessions Hub Specification
## Requirements
### Functional Requirements
1. **Sessions Tab**: Navigation item between Dashboard and Projects
2. **Active Sessions Display**: Show all running sessions with actions
3. **Last Session**: Prominently show last created/accessed session
4. **Quick Create**: Create sessions for any project from Sessions page
5. **Session Persistence**: Save last_session_id in user config
6. **Badge**: Show active session count in navigation
### Non-Functional Requirements
1. **Performance**: Load sessions in < 500ms
2. **Real-time**: Badge updates with active count
3. **Responsive**: Works on mobile and desktop
## API Specification
### Existing Endpoints Used
- `GET /users/me/sessions` - List all user sessions
- `POST /projects/{id}/repositories/{id}/instances` - Create instance
- `GET /projects` - List projects for selector
- `GET /projects/{id}/repositories` - List repos for selector
- `GET /tool-types` - List tool types for selector
- `GET /users/me/config` - Get user config (with last_session_id)
- `PATCH /users/me/config` - Update user config (last_session_id)
### User Config Schema Update
```python
class UserConfigUpdate(BaseModel):
theme: Optional[str] = None
default_editor: Optional[str] = None
git_user_name: Optional[str] = None
git_user_email: Optional[str] = None
last_session_id: Optional[str] = None # NEW
```
## UI Specification
### Sessions Page Layout
```
+------------------------------------------+
| Sessions [New Session]|
+------------------------------------------+
| |
| Last Session |
| +--------------------------------------+ |
| | VS Code Server - My Project [Open] | |
| | Running on port 8080 | |
| +--------------------------------------+ |
| |
| Active Sessions (3) |
| +----------+ +----------+ +----------+ |
| | Session 1| | Session 2| | Session 3| |
| | Running | | Running | | Running | |
| | [Open] | | [Open] | | [Open] | |
| +----------+ +----------+ +----------+ |
| |
| Recent Sessions |
| - Session 4 (stopped) |
| - Session 5 (stopped) |
| |
+------------------------------------------+
```
### Navigation Badge
```
[Dashboard] [Sessions (3)] [Projects] ...
```
Badge shows count of sessions with status === "running".
### Create Session Dialog
```
+------------------------------------------+
| Create New Session |
+------------------------------------------+
| Project: [Dropdown] |
| Repository: [Dropdown] |
| Tool Type: [Dropdown] |
| Name: [Input] |
| |
| [Cancel] [Create] |
+------------------------------------------+
```
## State Management
### Sessions Context (existing)
Already polls `/users/me/sessions` every 10s. Use this for:
- Active session count (badge)
- Active sessions list
- Recent sessions list
### User Config (existing)
Add `last_session_id` field. Update:
- On session creation
- On session open/resume
## Quality Gates
- TypeScript compilation passes
- ESLint passes
- All sessions load correctly
- Badge updates with active count
- Last session persists across reloads
- Create session works from Sessions page
+93
View File
@@ -0,0 +1,93 @@
# Sessions Hub - Tasks
## Phase 1: Backend Config Update
- [ ] **Task 1.1**: Update UserConfig model
- Add `last_session_id` field to `models/user_config.py`
- Create Alembic migration
- [ ] **Task 1.2**: Update config API
- Accept `last_session_id` in `api/user_config.py`
- Update Pydantic schemas
## Phase 2: Frontend Navigation
- [ ] **Task 2.1**: Add Sessions tab to AppShell
- Insert between Dashboard and Projects
- Add sessions icon
- Show badge with active count
- [ ] **Task 2.2**: Update router
- Add `/sessions` route
- Create placeholder page
## Phase 3: Sessions Page
- [ ] **Task 3.1**: Create SessionsPage component
- Page layout with sections
- Loading and error states
- [ ] **Task 3.2**: Implement Last Session section
- Fetch from user config
- Show session card with resume button
- Handle no last session state
- [ ] **Task 3.3**: Implement Active Sessions section
- Fetch from sessions context
- Grid of session cards
- Action buttons (Open, Stop, Restart, Delete)
- [ ] **Task 3.4**: Implement Recent Sessions section
- Show last 5 sessions
- Compact list view
- Status indicators
- [ ] **Task 3.5**: Implement Create Session section
- Project selector (fetch all projects)
- Repository selector (filtered by project)
- Tool type selector
- Display name input
- Create button with validation
## Phase 4: Session Actions
- [ ] **Task 4.1**: Resume last session
- Navigate to workspace
- Update user config
- [ ] **Task 4.2**: Open session
- Navigate to workspace with session
- [ ] **Task 4.3**: Create session
- Call API to create instance
- Update user config with last_session_id
- Refresh sessions list
## Phase 5: Polish
- [ ] **Task 5.1**: Add CSS styles
- Session cards layout
- Badge styling
- Responsive design
- [ ] **Task 5.2**: Add icons
- Session icon in navigation
- Action icons on cards
## Phase 6: Quality Gates
- [ ] **Task 6.1**: TypeScript check
- `npm run typecheck`
- [ ] **Task 6.2**: Lint check
- `npm run lint`
- [ ] **Task 6.3**: Build check
- `npm run build`
- [ ] **Task 6.4**: Manual verification
- Navigation shows Sessions tab
- Badge shows correct count
- Last session displays
- Can create session from page
- Config persists