e434c439c9
- Rename all component files to PascalCase matching exported names - Move components into feature directories (git/, session/, project/, terminal/, workspace/, ui/, layout/) - Rename all page files to PascalCase with Page suffix - Rename all API files to kebab-case - Update all imports across codebase with corrected relative depths - Preserve git history via git mv Quality gates: tsc (pass), eslint (pass), 66/74 tests pass (8 pre-existing failures) Refs: repo-restructure Task 4.4
215 lines
5.4 KiB
TypeScript
215 lines
5.4 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import { listProjects } from "../api/projects";
|
|
import { getUserSessions } from "../api/sessions";
|
|
import { listToolTypes } from "../api/tool-types";
|
|
import { getUserConfig } from "../api/settings";
|
|
import { Icon } from "../components/ui/Icon";
|
|
import { LoadingState, ErrorState } from "../components/ui";
|
|
import { CreateSessionForm, SessionList } from "../components/features/session";
|
|
import type { Project } from "../types/project";
|
|
import type { Session } from "../types/session";
|
|
import type { ToolType } from "../types/tool-type";
|
|
|
|
type SessionsStatus = "loading" | "ready" | "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 [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
|
|
|
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 {
|
|
setProjects(await listProjects());
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
};
|
|
void loadProjects();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const loadToolTypes = async () => {
|
|
try {
|
|
setToolTypes(await listToolTypes());
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
};
|
|
void loadToolTypes();
|
|
}, []);
|
|
|
|
const activeSessions = useMemo(
|
|
() =>
|
|
sessions.filter((s) =>
|
|
["running", "building", "pending"].includes(s.status),
|
|
),
|
|
[sessions],
|
|
);
|
|
|
|
const recentSessions = useMemo(
|
|
() =>
|
|
sessions
|
|
.filter((s) => ["stopped", "error"].includes(s.status))
|
|
.slice(0, 5),
|
|
[sessions],
|
|
);
|
|
|
|
const lastSession = useMemo(
|
|
() => sessions.find((s) => s.id === lastSessionId) ?? null,
|
|
[sessions, lastSessionId],
|
|
);
|
|
|
|
const handleOpen = useCallback(
|
|
(session: Session) => {
|
|
if (session.tool_type_interfaces?.includes("terminal")) {
|
|
navigate(`/instances/${session.id}/terminal`);
|
|
} else {
|
|
navigate(`/projects/${session.project_id}`);
|
|
}
|
|
},
|
|
[navigate],
|
|
);
|
|
|
|
const handleResumeLast = useCallback(() => {
|
|
if (!lastSession) return;
|
|
const project = projects.find((p) => p.name === lastSession.project_name);
|
|
if (project) navigate(`/projects/${project.id}`);
|
|
}, [lastSession, projects, navigate]);
|
|
|
|
return (
|
|
<section className="stack sessions-page">
|
|
<div className="page-header">
|
|
<h1>Sessions</h1>
|
|
</div>
|
|
|
|
{status === "loading" && <LoadingState message="Loading sessions..." />}
|
|
|
|
{status === "error" && (
|
|
<ErrorState
|
|
message="Failed to load sessions"
|
|
onRetry={() => void loadSessions()}
|
|
/>
|
|
)}
|
|
|
|
{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 ||
|
|
lastSession.tool_type_name ||
|
|
"Unnamed Session"}
|
|
</h3>
|
|
<p className="muted">
|
|
{lastSession.tool_type_name} · {lastSession.project_name} ·{" "}
|
|
{lastSession.repository_name}
|
|
</p>
|
|
{lastSession.url && (
|
|
<p className="session-url">
|
|
<a
|
|
href={lastSession.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{lastSession.url}
|
|
</a>
|
|
</p>
|
|
)}
|
|
<span className={`status-badge ${lastSession.status}`}>
|
|
{lastSession.status}
|
|
</span>
|
|
</div>
|
|
<div className="last-session-actions">
|
|
{lastSession.url ? (
|
|
<a
|
|
href={lastSession.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="primary-button"
|
|
>
|
|
<Icon name="external" size="sm" /> Open
|
|
</a>
|
|
) : (
|
|
<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>
|
|
<SessionList
|
|
sessions={activeSessions}
|
|
variant="active"
|
|
onSessionChange={loadSessions}
|
|
onOpen={handleOpen}
|
|
/>
|
|
</div>
|
|
|
|
{/* Recent Sessions */}
|
|
{recentSessions.length > 0 && (
|
|
<div className="recent-sessions-section">
|
|
<h2>Recent Sessions</h2>
|
|
<SessionList
|
|
sessions={recentSessions}
|
|
variant="recent"
|
|
onSessionChange={loadSessions}
|
|
onOpen={handleOpen}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Session */}
|
|
<CreateSessionForm
|
|
projects={projects}
|
|
toolTypes={toolTypes}
|
|
onCreated={loadSessions}
|
|
/>
|
|
</>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|