No projects yet.
+
{projects.map((project) => (
diff --git a/apps/web/src/pages/git-history.tsx b/apps/web/src/pages/git-history.tsx
index beb27ec..33113a3 100644
--- a/apps/web/src/pages/git-history.tsx
+++ b/apps/web/src/pages/git-history.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { getCommitDetail, getRepositoryHistory, type CommitDetail, type CommitHistoryEntry, type CommitHistoryResponse } from "../api/git_repositories";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAsyncData } from "../hooks/use-async-data";
@@ -54,7 +55,7 @@ export const GitHistoryPage = () => {
if (status === "loading") {
return (
- Loading commit history...
+
);
}
@@ -62,11 +63,7 @@ export const GitHistoryPage = () => {
if (status === "error") {
return (
- Failed to load commit history
-
+
);
}
diff --git a/apps/web/src/pages/git-repositories.tsx b/apps/web/src/pages/git-repositories.tsx
index 5f2c195..7be70d0 100644
--- a/apps/web/src/pages/git-repositories.tsx
+++ b/apps/web/src/pages/git-repositories.tsx
@@ -6,6 +6,7 @@ import {
listRepositories,
} from "../api/git_repositories";
import type { GitRepository } from "../api/git_repositories";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
import { useAsyncData } from "../hooks/use-async-data";
@@ -48,19 +49,11 @@ export const GitRepositoriesPage = () => {
- {status === "loading" && Loading repositories...
}
+ {status === "loading" && No repositories yet. Create your first repository above.
}
+ {isEmpty &&
diff --git a/apps/web/src/pages/profile.tsx b/apps/web/src/pages/profile.tsx
index 3f48ccd..a32bd9b 100644
--- a/apps/web/src/pages/profile.tsx
+++ b/apps/web/src/pages/profile.tsx
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { getProfile, updateProfile, uploadAvatar } from "../api/profile";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAuth } from "../state/auth";
import { useAsyncData } from "../hooks/use-async-data";
@@ -91,17 +92,9 @@ export const ProfilePage = () => {
Profile
- {displayStatus === "loading" && Loading profile...
}
+ {displayStatus === "loading" && }
- {displayStatus === "error" && (
-
-
Failed to load profile
-
-
- )}
+ {displayStatus === "error" && }
{(displayStatus === "ready" || displayStatus === "saving") && profile && (
diff --git a/apps/web/src/pages/projects.tsx b/apps/web/src/pages/projects.tsx
index 1420ae3..c1c0d71 100644
--- a/apps/web/src/pages/projects.tsx
+++ b/apps/web/src/pages/projects.tsx
@@ -10,6 +10,7 @@ import {
type ProjectCreateInput,
type ProjectUpdateInput,
} from "../api/projects";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useAsyncData } from "../hooks/use-async-data";
import type { Project } from "../types";
@@ -101,19 +102,11 @@ export const ProjectsPage = () => {
- {status === "loading" && Loading projects...
}
+ {status === "loading" && }
- {status === "error" && (
-
-
Failed to load projects
-
-
- )}
+ {status === "error" && }
- {isEmpty && No projects yet. Create your first project above.
}
+ {isEmpty && }
{status === "ready" && safeProjects.length > 0 && (
diff --git a/apps/web/src/pages/repo-workspace.tsx b/apps/web/src/pages/repo-workspace.tsx
index 68fe8ef..a1027e4 100644
--- a/apps/web/src/pages/repo-workspace.tsx
+++ b/apps/web/src/pages/repo-workspace.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
@@ -164,26 +165,16 @@ export const RepoWorkspace = () => {
)}
{status === "loading" && (
-
Loading repositories...
+
)}
{status === "error" && (
-
-
Failed to load repositories
-
-
+
void loadRepositories()} />
)}
{status === "empty" && (
-
No repositories in this project yet.
+
)}
{entries.length === 0 && (
-
No files in this repository yet.
+
)}
{entries.map((entry) => {
const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null;
diff --git a/apps/web/src/pages/sessions.tsx b/apps/web/src/pages/sessions.tsx
index 22ac603..e7c0529 100644
--- a/apps/web/src/pages/sessions.tsx
+++ b/apps/web/src/pages/sessions.tsx
@@ -7,18 +7,15 @@ import { listRepositories, type GitRepository } from "../api/git_repositories";
import {
getUserSessions,
type Session,
- deleteInstance,
- stopInstance,
- startInstance,
checkInstanceHealth,
- recreateInstanceTunnel,
} from "../api/sessions";
import { listToolTypes, type ToolType } from "../api/tool_types";
import { getUserConfig, updateUserConfig } from "../api/settings";
-import { Icon } from "../components/icon";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { CreateSessionForm } from "../components/create-session-form";
import { SessionList } from "../components/session-list";
import { SessionCard } from "../components/session-card";
+import { useInstanceActions } from "../hooks/use-instance-actions";
import type { InstanceHealth } from "../api/sessions";
type SessionsStatus = "loading" | "ready" | "error";
@@ -34,11 +31,7 @@ export const SessionsPage = () => {
const [toolTypes, setToolTypes] = useState
([]);
const [selectedProject, setSelectedProject] = useState("");
- const [dirtyDeleteSession, setDirtyDeleteSession] = useState(null);
- const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState([]);
-
const [tunnelHealth, setTunnelHealth] = useState>({});
- const [loadingSessionId, setLoadingSessionId] = useState(null);
const loadSessions = useCallback(async () => {
setStatus("loading");
@@ -83,7 +76,18 @@ export const SessionsPage = () => {
void loadToolTypes();
}, []);
-
+ const {
+ loadingSessionId,
+ dirtyDeleteSession,
+ dirtyDeleteFiles,
+ handleOpen,
+ handleStart,
+ handleStop,
+ handleDelete,
+ handleForceDelete,
+ handleRecreateTunnel,
+ clearDirtyDelete,
+ } = useInstanceActions({ onRefresh: loadSessions });
// Poll health every 30 seconds for active web-enabled instances
useEffect(() => {
@@ -154,116 +158,15 @@ export const SessionsPage = () => {
await loadSessions();
};
- const handleStop = async (session: Session) => {
- if (loadingSessionId === session.id) return;
- setLoadingSessionId(session.id);
- try {
- await stopInstance(session.project_id, session.repository_id, session.id);
- await loadSessions();
- } catch {
- // ignore
- } finally {
- setLoadingSessionId(null);
- }
- };
-
- const handleDelete = async (session: Session) => {
- if (loadingSessionId === session.id) return;
- setLoadingSessionId(session.id);
- try {
- await deleteInstance(session.project_id, session.repository_id, session.id);
- setDirtyDeleteSession(null);
- setDirtyDeleteFiles([]);
- // Remove from local state immediately
- setSessions((prev) => prev.filter((s) => s.id !== session.id));
- } catch (error) {
- const axiosError = error as { response?: { status?: number; data?: { detail?: { changed_files?: string[] } } } };
- if (axiosError.response?.status === 409) {
- const detail = axiosError.response.data?.detail;
- if (detail?.changed_files) {
- setDirtyDeleteSession(session);
- setDirtyDeleteFiles(detail.changed_files);
- return;
- }
- }
- } finally {
- setLoadingSessionId(null);
- }
- };
-
- const handleForceDelete = async (session: Session) => {
- if (loadingSessionId === session.id) return;
- setLoadingSessionId(session.id);
- try {
- await deleteInstance(session.project_id, session.repository_id, session.id, true);
- setDirtyDeleteSession(null);
- setDirtyDeleteFiles([]);
- setSessions((prev) => prev.filter((s) => s.id !== session.id));
- } catch {
- // ignore
- } finally {
- setLoadingSessionId(null);
- }
- };
-
- const handleRecreateTunnel = async (session: Session) => {
- if (loadingSessionId === session.id) return;
- setLoadingSessionId(session.id);
- try {
- await recreateInstanceTunnel(
- session.project_id,
- session.repository_id,
- session.id
- );
- // Refresh sessions to get new URL
- await loadSessions();
- } catch {
- // ignore
- } finally {
- setLoadingSessionId(null);
- }
- };
-
- const handleStart = async (session: Session) => {
- if (loadingSessionId === session.id) return;
- setLoadingSessionId(session.id);
- try {
- await startInstance(session.project_id, session.repository_id, session.id);
- await loadSessions();
- } catch {
- // ignore
- } finally {
- setLoadingSessionId(null);
- }
- };
-
- const handleOpen = (session: Session) => {
- if (session.url) {
- window.open(session.url, '_blank', 'noopener,noreferrer');
- } else if (session.tool_type_interfaces?.includes("terminal")) {
- navigate(`/instances/${session.id}/terminal`);
- } else {
- navigate(`/projects/${session.project_id}`);
- }
- };
-
return (
Sessions
- {status === "loading" && Loading sessions...
}
+ {status === "loading" && }
- {status === "error" && (
-
-
Failed to load sessions
-
-
- )}
+ {status === "error" && void loadSessions()} />}
{status === "ready" && (
<>
@@ -311,7 +214,7 @@ export const SessionsPage = () => {
{/* Dirty Delete Confirmation Modal */}
{dirtyDeleteSession && (
- setDirtyDeleteSession(null)}>
+
e.stopPropagation()}>
Uncommitted Changes
@@ -330,7 +233,7 @@ export const SessionsPage = () => {
- {status === "error" && (
-
-
Failed to load SSH keys
-
loadKeys()} type="button">
-
- Retry
-
-
- )}
+ {status === "error" &&
}
{safeKeys.length === 0 ? (
-
No SSH keys yet. Generate one above.
+
) : (
safeKeys.map((key) => (
diff --git a/apps/web/src/pages/tool-workshop.tsx b/apps/web/src/pages/tool-workshop.tsx
index 814003b..8cc295c 100644
--- a/apps/web/src/pages/tool-workshop.tsx
+++ b/apps/web/src/pages/tool-workshop.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useState } from "react";
+import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
import { Icon } from "../components/icon";
import { useMobileViewport } from "../hooks/use-mobile-viewport";
import { extractErrorMessage } from "../utils/errors";
@@ -491,7 +492,7 @@ export const ToolWorkshopPage = () => {
if (status === "loading") {
return (
-
Loading Tool Workshop...
+
);
}
@@ -499,10 +500,7 @@ export const ToolWorkshopPage = () => {
if (status === "error") {
return (
-
Failed to load Tool Workshop.
-
- Retry
-
+
);
}
@@ -1305,7 +1303,7 @@ export const ToolWorkshopPage = () => {
{toolConfigs.length === 0 ? (
-
No configurations for this tool type yet.
+
) : (
toolConfigs.map((config) => (