diff --git a/apps/web/src/api/config_folders.ts b/apps/web/src/api/config_folders.ts index 5918f71..890111a 100644 --- a/apps/web/src/api/config_folders.ts +++ b/apps/web/src/api/config_folders.ts @@ -1,77 +1,77 @@ import { apiClient } from "./client"; import type { - ConfigFolder, - CreateConfigFolderRequest, - UpdateConfigFolderRequest, - ProjectOverrideRequest, + ConfigFolder, + CreateConfigFolderRequest, + UpdateConfigFolderRequest, + ProjectOverrideRequest, } from "../types/config-folder"; export type { - ConfigFolder, - CreateConfigFolderRequest, - UpdateConfigFolderRequest, - ProjectOverrideRequest, + ConfigFolder, + CreateConfigFolderRequest, + UpdateConfigFolderRequest, + ProjectOverrideRequest, } from "../types/config-folder"; export const listConfigFolders = async (): Promise => { - const response = await apiClient.get("/config-folders"); - return response.data; + const response = await apiClient.get("/config-folders"); + return response.data; }; export const getConfigFolder = async (id: string): Promise => { - const response = await apiClient.get(`/config-folders/${id}`); - return response.data; + const response = await apiClient.get(`/config-folders/${id}`); + return response.data; }; export const createConfigFolder = async ( - data: CreateConfigFolderRequest + data: CreateConfigFolderRequest, ): Promise => { - const response = await apiClient.post("/config-folders", data); - return response.data; + const response = await apiClient.post("/config-folders", data); + return response.data; }; export const updateConfigFolder = async ( - id: string, - data: UpdateConfigFolderRequest + id: string, + data: UpdateConfigFolderRequest, ): Promise => { - const response = await apiClient.put( - `/config-folders/${id}`, - data - ); - return response.data; + const response = await apiClient.put( + `/config-folders/${id}`, + data, + ); + return response.data; }; export const deleteConfigFolder = async (id: string): Promise => { - await apiClient.delete(`/config-folders/${id}`); + await apiClient.delete(`/config-folders/${id}`); }; export const addProjectOverride = async ( - id: string, - projectId: string, - data: ProjectOverrideRequest + id: string, + projectId: string, + data: ProjectOverrideRequest, ): Promise => { - const response = await apiClient.post( - `/config-folders/${id}/overrides/${projectId}`, - data - ); - return response.data; + const response = await apiClient.post( + `/config-folders/${id}/overrides/${projectId}`, + data, + ); + return response.data; }; export const updateProjectOverride = async ( - id: string, - projectId: string, - data: ProjectOverrideRequest + id: string, + projectId: string, + data: ProjectOverrideRequest, ): Promise => { - const response = await apiClient.put( - `/config-folders/${id}/overrides/${projectId}`, - data - ); - return response.data; + const response = await apiClient.put( + `/config-folders/${id}/overrides/${projectId}`, + data, + ); + return response.data; }; export const deleteProjectOverride = async ( - id: string, - projectId: string + id: string, + projectId: string, ): Promise => { - await apiClient.delete(`/config-folders/${id}/overrides/${projectId}`); + await apiClient.delete(`/config-folders/${id}/overrides/${projectId}`); }; diff --git a/apps/web/src/api/git_repositories.ts b/apps/web/src/api/git_repositories.ts index dbb6f9e..a6a6c7d 100644 --- a/apps/web/src/api/git_repositories.ts +++ b/apps/web/src/api/git_repositories.ts @@ -1,179 +1,191 @@ import { apiClient } from "./client"; import type { - CommitDetail, - CommitHistoryResponse, - CommitResponse, - GitRepository, - GitRepositoryCreate, - GitStatus, - MergeResponse, - URLParseResult, + CommitDetail, + CommitHistoryResponse, + CommitResponse, + GitRepository, + GitRepositoryCreate, + GitStatus, + MergeResponse, + URLParseResult, } from "../types/git-repository"; export type { - CommitDetail, - CommitHistoryEntry, - CommitHistoryResponse, - CommitResponse, - GitRepository, - GitRepositoryCreate, - GitStatus, - MergeResponse, - URLParseResult, + CommitDetail, + CommitHistoryEntry, + CommitHistoryResponse, + CommitResponse, + GitRepository, + GitRepositoryCreate, + GitStatus, + MergeResponse, + URLParseResult, } from "../types/git-repository"; export async function parseGitUrl(url: string): Promise { - const response = await apiClient.post("/projects/repositories/parse-url", { url }); - return response.data; + const response = await apiClient.post("/projects/repositories/parse-url", { + url, + }); + return response.data; } -export async function listRepositories(projectId: string): Promise { - const response = await apiClient.get(`/projects/${projectId}/repositories`); - return response.data; +export async function listRepositories( + projectId: string, +): Promise { + const response = await apiClient.get(`/projects/${projectId}/repositories`); + return response.data; } export async function createRepository( - projectId: string, - data: GitRepositoryCreate + projectId: string, + data: GitRepositoryCreate, ): Promise { - const response = await apiClient.post(`/projects/${projectId}/repositories`, data); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories`, + data, + ); + return response.data; } -export async function deleteRepository(projectId: string, repoId: string): Promise { - await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`); +export async function deleteRepository( + projectId: string, + repoId: string, +): Promise { + await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`); } export async function getRepositoryHistory( - projectId: string, - repoId: string, - branch?: string, - limit?: number + projectId: string, + repoId: string, + branch?: string, + limit?: number, ): Promise { - const searchParams = new URLSearchParams(); - if (branch) searchParams.set("branch", branch); - if (limit) searchParams.set("limit", String(limit)); - const queryString = searchParams.toString(); - const params = queryString ? `?${queryString}` : ""; - const response = await apiClient.get(`/projects/${projectId}/repositories/${repoId}/history${params}`); - return response.data; + const searchParams = new URLSearchParams(); + if (branch) searchParams.set("branch", branch); + if (limit) searchParams.set("limit", String(limit)); + const queryString = searchParams.toString(); + const params = queryString ? `?${queryString}` : ""; + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/history${params}`, + ); + return response.data; } export async function getCommitDetail( - projectId: string, - repoId: string, - commitHash: string + projectId: string, + repoId: string, + commitHash: string, ): Promise { - const response = await apiClient.get( - `/projects/${projectId}/repositories/${repoId}/commits/${commitHash}` - ); - return response.data; + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/commits/${commitHash}`, + ); + return response.data; } export async function getRepositoryStatus( - projectId: string, - repoId: string + projectId: string, + repoId: string, ): Promise { - const response = await apiClient.get( - `/projects/${projectId}/repositories/${repoId}/status` - ); - return response.data; + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/status`, + ); + return response.data; } export async function createBranch( - projectId: string, - repoId: string, - name: string, - baseBranch: string = "HEAD" + projectId: string, + repoId: string, + name: string, + baseBranch: string = "HEAD", ): Promise<{ message: string; branch: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/branches`, - { name, base_branch: baseBranch } - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/branches`, + { name, base_branch: baseBranch }, + ); + return response.data; } export async function deleteBranch( - projectId: string, - repoId: string, - branchName: string, - force: boolean = false + projectId: string, + repoId: string, + branchName: string, + force: boolean = false, ): Promise<{ message: string }> { - const response = await apiClient.delete( - `/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}` - ); - return response.data; + const response = await apiClient.delete( + `/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}`, + ); + return response.data; } export async function checkoutBranch( - projectId: string, - repoId: string, - branch: string + projectId: string, + repoId: string, + branch: string, ): Promise<{ message: string; branch: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/checkout`, - { branch } - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/checkout`, + { branch }, + ); + return response.data; } export async function commitChanges( - projectId: string, - repoId: string, - message: string, - files?: string[] + projectId: string, + repoId: string, + message: string, + files?: string[], ): Promise { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/commit`, - { message, files } - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/commit`, + { message, files }, + ); + return response.data; } export async function fetchRepository( - projectId: string, - repoId: string + projectId: string, + repoId: string, ): Promise<{ message: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/fetch` - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/fetch`, + ); + return response.data; } export async function pullRepository( - projectId: string, - repoId: string, - branch?: string + projectId: string, + repoId: string, + branch?: string, ): Promise<{ message: string }> { - const params = branch ? `?branch=${branch}` : ""; - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/pull${params}` - ); - return response.data; + const params = branch ? `?branch=${branch}` : ""; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/pull${params}`, + ); + return response.data; } export async function pushRepository( - projectId: string, - repoId: string, - branch?: string + projectId: string, + repoId: string, + branch?: string, ): Promise<{ message: string }> { - const params = branch ? `?branch=${branch}` : ""; - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/push${params}` - ); - return response.data; + const params = branch ? `?branch=${branch}` : ""; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/push${params}`, + ); + return response.data; } export async function mergeBranches( - projectId: string, - repoId: string, - sourceBranch: string, - targetBranch?: string, - message?: string + projectId: string, + repoId: string, + sourceBranch: string, + targetBranch?: string, + message?: string, ): Promise { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/merge`, - { source_branch: sourceBranch, target_branch: targetBranch, message } - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/merge`, + { source_branch: sourceBranch, target_branch: targetBranch, message }, + ); + return response.data; } diff --git a/apps/web/src/api/sessions.ts b/apps/web/src/api/sessions.ts index 9124774..2d4eaf2 100644 --- a/apps/web/src/api/sessions.ts +++ b/apps/web/src/api/sessions.ts @@ -6,97 +6,97 @@ export type { Session } from "../types/session"; export type { ToolInstance } from "../types/tool-instance"; export async function listInstances( - projectId: string, - repoId: string + projectId: string, + repoId: string, ): Promise { - const response = await apiClient.get( - `/projects/${projectId}/repositories/${repoId}/instances` - ); - return response.data.instances; + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/instances`, + ); + return response.data.instances; } export async function createInstance( - projectId: string, - repoId: string, - toolTypeId: string, - displayName?: string + projectId: string, + repoId: string, + toolTypeId: string, + displayName?: string, ): Promise { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/instances`, - { - tool_type_id: toolTypeId, - display_name: displayName, - } - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/instances`, + { + tool_type_id: toolTypeId, + display_name: displayName, + }, + ); + return response.data; } export async function startInstance( - projectId: string, - repoId: string, - instanceId: string + projectId: string, + repoId: string, + instanceId: string, ): Promise<{ status: string; url?: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start` - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`, + ); + return response.data; } export async function stopInstance( - projectId: string, - repoId: string, - instanceId: string + projectId: string, + repoId: string, + instanceId: string, ): Promise<{ status: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/stop` - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/stop`, + ); + return response.data; } export async function restartInstance( - projectId: string, - repoId: string, - instanceId: string + projectId: string, + repoId: string, + instanceId: string, ): Promise<{ status: string; url?: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart` - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`, + ); + return response.data; } export async function deleteInstance( - projectId: string, - repoId: string, - instanceId: string + projectId: string, + repoId: string, + instanceId: string, ): Promise { - await apiClient.delete( - `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}` - ); + await apiClient.delete( + `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`, + ); } export async function getUserSessions(): Promise { - const response = await apiClient.get("/users/me/sessions"); - return response.data.sessions; + const response = await apiClient.get("/users/me/sessions"); + return response.data.sessions; } export async function checkInstanceHealth( - projectId: string, - repoId: string, - instanceId: string + projectId: string, + repoId: string, + instanceId: string, ): Promise<{ healthy: boolean; status_code: number | null; error?: string }> { - const response = await apiClient.get( - `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health` - ); - return response.data; + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health`, + ); + return response.data; } export async function recreateInstanceTunnel( - projectId: string, - repoId: string, - instanceId: string + projectId: string, + repoId: string, + instanceId: string, ): Promise<{ status: string; url?: string }> { - const response = await apiClient.post( - `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/recreate-tunnel` - ); - return response.data; + const response = await apiClient.post( + `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/recreate-tunnel`, + ); + return response.data; } diff --git a/apps/web/src/api/tool_configs.ts b/apps/web/src/api/tool_configs.ts index 0b056e7..3974f30 100644 --- a/apps/web/src/api/tool_configs.ts +++ b/apps/web/src/api/tool_configs.ts @@ -4,46 +4,49 @@ import type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config"; export type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config"; export const listToolConfigs = async ( - tool_type_id?: string, - project_id?: string + tool_type_id?: string, + project_id?: string, ): Promise => { - const params = new URLSearchParams(); - if (tool_type_id) params.append("tool_type_id", tool_type_id); - if (project_id) params.append("project_id", project_id); + const params = new URLSearchParams(); + if (tool_type_id) params.append("tool_type_id", tool_type_id); + if (project_id) params.append("project_id", project_id); - const response = await apiClient.get<{ configs: ToolConfig[] }>( - `/tool-configs?${params.toString()}` - ); - return response.data.configs; + const response = await apiClient.get<{ configs: ToolConfig[] }>( + `/tool-configs?${params.toString()}`, + ); + return response.data.configs; }; export const createToolConfig = async ( - data: CreateToolConfigRequest + data: CreateToolConfigRequest, ): Promise => { - const response = await apiClient.post<{ configs: ToolConfig[] }>("/tool-configs", data); - return response.data.configs[0]; + const response = await apiClient.post<{ configs: ToolConfig[] }>( + "/tool-configs", + data, + ); + return response.data.configs[0]; }; export const updateToolConfig = async ( - id: string, - data: CreateToolConfigRequest + id: string, + data: CreateToolConfigRequest, ): Promise => { - const response = await apiClient.put<{ configs: ToolConfig[] }>( - `/tool-configs/${id}`, - data - ); - return response.data.configs[0]; + const response = await apiClient.put<{ configs: ToolConfig[] }>( + `/tool-configs/${id}`, + data, + ); + return response.data.configs[0]; }; export const deleteToolConfig = async (id: string): Promise => { - await apiClient.delete(`/tool-configs/${id}`); + await apiClient.delete(`/tool-configs/${id}`); }; export const getToolConfigDefaults = async ( - toolTypeId: string + toolTypeId: string, ): Promise => { - const response = await apiClient.get( - `/tool-configs/defaults/${toolTypeId}` - ); - return response.data; + const response = await apiClient.get( + `/tool-configs/defaults/${toolTypeId}`, + ); + return response.data; }; diff --git a/apps/web/src/api/tool_types.ts b/apps/web/src/api/tool_types.ts index 21d060d..54d3fa7 100644 --- a/apps/web/src/api/tool_types.ts +++ b/apps/web/src/api/tool_types.ts @@ -1,33 +1,51 @@ import { apiClient } from "./client"; -import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type"; +import type { + ToolType, + CreateToolTypeRequest, + UpdateToolTypeRequest, +} from "../types/tool-type"; -export type { ReadinessProbe, ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type"; +export type { + ReadinessProbe, + ToolType, + CreateToolTypeRequest, + UpdateToolTypeRequest, +} from "../types/tool-type"; export const listToolTypes = async (): Promise => { - const response = await apiClient.get("/tool-types"); - return response.data; + const response = await apiClient.get("/tool-types"); + return response.data; }; export const getToolType = async (id: string): Promise => { - const response = await apiClient.get(`/tool-types/${id}`); - return response.data; + const response = await apiClient.get(`/tool-types/${id}`); + return response.data; }; -export const createToolType = async (data: CreateToolTypeRequest): Promise => { - const response = await apiClient.post("/tool-types", data); - return response.data; +export const createToolType = async ( + data: CreateToolTypeRequest, +): Promise => { + const response = await apiClient.post("/tool-types", data); + return response.data; }; -export const updateToolType = async (id: string, data: UpdateToolTypeRequest): Promise => { - const response = await apiClient.put(`/tool-types/${id}`, data); - return response.data; +export const updateToolType = async ( + id: string, + data: UpdateToolTypeRequest, +): Promise => { + const response = await apiClient.put(`/tool-types/${id}`, data); + return response.data; }; export const deleteToolType = async (id: string): Promise => { - await apiClient.delete(`/tool-types/${id}`); + await apiClient.delete(`/tool-types/${id}`); }; -export const validateToolType = async (id: string): Promise<{ valid: boolean; errors?: string[] }> => { - const response = await apiClient.get<{ valid: boolean; errors?: string[] }>(`/tool-types/${id}/validate`); - return response.data; +export const validateToolType = async ( + id: string, +): Promise<{ valid: boolean; errors?: string[] }> => { + const response = await apiClient.get<{ valid: boolean; errors?: string[] }>( + `/tool-types/${id}/validate`, + ); + return response.data; }; diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index c1762b0..5e65cce 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -10,117 +10,123 @@ import { Icon } from "./icon"; import type { IconName } from "../utils/icons"; const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [ - { to: "/", label: "Home", icon: "dashboard" }, - { to: "/projects", label: "Projects", icon: "projects" }, - { to: "/tool-workshop", label: "Tool Workshop", icon: "settings" }, - { to: "/settings", label: "Settings", icon: "settings" } + { to: "/", label: "Home", icon: "dashboard" }, + { to: "/projects", label: "Projects", icon: "projects" }, + { to: "/tool-workshop", label: "Tool Workshop", icon: "settings" }, + { to: "/settings", label: "Settings", icon: "settings" }, ]; const ACTIVE_STATUSES = ["running", "building", "pending"]; const SessionItem = ({ session }: { session: Session }) => { - const isRunning = session.status === "running"; - const displayName = session.display_name || session.tool_type_name || "Unnamed Session"; + const isRunning = session.status === "running"; + const displayName = + session.display_name || session.tool_type_name || "Unnamed Session"; - return ( - - - - {displayName} - - ); + return ( + + + + {displayName} + + ); }; export const AppShell = () => { - useTheme(); - const { user, logout } = useAuth(); - const { sessions, setAllSessions } = useSessions(); + useTheme(); + const { user, logout } = useAuth(); + const { sessions, setAllSessions } = useSessions(); - const loadSessions = useCallback(async () => { - try { - const data = await getUserSessions(); - setAllSessions(data); - } catch { - // Silently fail - sessions are optional - } - }, [setAllSessions]); + const loadSessions = useCallback(async () => { + try { + const data = await getUserSessions(); + setAllSessions(data); + } catch { + // Silently fail - sessions are optional + } + }, [setAllSessions]); - useEffect(() => { - void loadSessions(); - // Poll every 10 seconds - const interval = setInterval(() => { - void loadSessions(); - }, 10000); - return () => clearInterval(interval); - }, [loadSessions]); + useEffect(() => { + void loadSessions(); + // Poll every 10 seconds + const interval = setInterval(() => { + void loadSessions(); + }, 10000); + return () => clearInterval(interval); + }, [loadSessions]); - return ( -
-
- - Headquarter - -
- - {user?.name ?? "User"} - - -
-
+ return ( +
+
+ + Headquarter + +
+ + {user?.name ?? "User"} + + +
+
-
- +
+
-
- ); + {sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length > + 0 && ( + <> +
+
Live sessions
+ {sessions + .filter((s) => ACTIVE_STATUSES.includes(s.status)) + .map((session) => ( + + ))} + + )} + + +
+ +
+
+
+ ); }; diff --git a/apps/web/src/components/instance-list.tsx b/apps/web/src/components/instance-list.tsx index 4036c1b..d5c87d8 100644 --- a/apps/web/src/components/instance-list.tsx +++ b/apps/web/src/components/instance-list.tsx @@ -4,356 +4,385 @@ import { Icon } from "./icon"; import type { ToolInstance } from "../types/tool-instance"; import type { ToolType } from "../types/tool-type"; import { - checkInstanceHealth, - createInstance, - deleteInstance, - listInstances, - recreateInstanceTunnel, - restartInstance, - startInstance, - stopInstance, + checkInstanceHealth, + createInstance, + deleteInstance, + listInstances, + recreateInstanceTunnel, + restartInstance, + startInstance, + stopInstance, } from "../api/sessions"; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; +const API_BASE_URL = + import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000"; interface InstanceListProps { - projectId: string; - repoId: string; - toolTypes: ToolType[]; + projectId: string; + repoId: string; + toolTypes: ToolType[]; } -export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps) => { - const navigate = useNavigate(); - const [instances, setInstances] = useState([]); - const [loading, setLoading] = useState(false); - const [showCreate, setShowCreate] = useState(false); - const [selectedToolType, setSelectedToolType] = useState(""); - const [displayName, setDisplayName] = useState(""); - const [error, setError] = useState(null); - - // Stop confirmation - const [stopConfirmId, setStopConfirmId] = useState(null); - - // Health check state - const [healthStatus, setHealthStatus] = useState>({}); +export const InstanceList = ({ + projectId, + repoId, + toolTypes, +}: InstanceListProps) => { + const navigate = useNavigate(); + const [instances, setInstances] = useState([]); + const [loading, setLoading] = useState(false); + const [showCreate, setShowCreate] = useState(false); + const [selectedToolType, setSelectedToolType] = useState(""); + const [displayName, setDisplayName] = useState(""); + const [error, setError] = useState(null); - const loadInstances = useCallback(async () => { - setLoading(true); - try { - const data = await listInstances(projectId, repoId); - setInstances(data); - } catch { - setError("Failed to load instances"); - } finally { - setLoading(false); - } - }, [projectId, repoId]); + // Stop confirmation + const [stopConfirmId, setStopConfirmId] = useState(null); - useEffect(() => { - void loadInstances(); - }, [loadInstances]); + // Health check state + const [healthStatus, setHealthStatus] = useState< + Record + >({}); - // Health check polling - useEffect(() => { - const runningInstances = instances.filter(i => i.status === "running" && i.url?.startsWith("http")); - if (runningInstances.length === 0) return; + const loadInstances = useCallback(async () => { + setLoading(true); + try { + const data = await listInstances(projectId, repoId); + setInstances(data); + } catch { + setError("Failed to load instances"); + } finally { + setLoading(false); + } + }, [projectId, repoId]); - const checkHealth = async () => { - for (const instance of runningInstances) { - try { - const health = await checkInstanceHealth(projectId, repoId, instance.id); - setHealthStatus(prev => ({ - ...prev, - [instance.id]: { healthy: health.healthy, lastCheck: Date.now() } - })); - } catch { - setHealthStatus(prev => ({ - ...prev, - [instance.id]: { healthy: false, lastCheck: Date.now() } - })); - } - } - }; + useEffect(() => { + void loadInstances(); + }, [loadInstances]); - // Check immediately - void checkHealth(); - - // Then every 30 seconds - const interval = setInterval(() => void checkHealth(), 30000); - return () => clearInterval(interval); - }, [instances, projectId, repoId]); + // Health check polling + useEffect(() => { + const runningInstances = instances.filter( + (i) => i.status === "running" && i.url?.startsWith("http"), + ); + if (runningInstances.length === 0) return; - const handleCreate = async () => { - if (!selectedToolType) return; - setError(null); - try { - await createInstance(projectId, repoId, selectedToolType, displayName || undefined); - setShowCreate(false); - setSelectedToolType(""); - setDisplayName(""); - await loadInstances(); - } catch { - setError("Failed to create instance"); - } - }; + const checkHealth = async () => { + for (const instance of runningInstances) { + try { + const health = await checkInstanceHealth( + projectId, + repoId, + instance.id, + ); + setHealthStatus((prev) => ({ + ...prev, + [instance.id]: { healthy: health.healthy, lastCheck: Date.now() }, + })); + } catch { + setHealthStatus((prev) => ({ + ...prev, + [instance.id]: { healthy: false, lastCheck: Date.now() }, + })); + } + } + }; - const handleStart = async (instanceId: string) => { - try { - await startInstance(projectId, repoId, instanceId); - await loadInstances(); - } catch { - setError("Failed to start instance"); - } - }; + // Check immediately + void checkHealth(); - const handleStop = async (instanceId: string) => { - try { - await stopInstance(projectId, repoId, instanceId); - setStopConfirmId(null); - await loadInstances(); - } catch { - setError("Failed to stop instance"); - } - }; + // Then every 30 seconds + const interval = setInterval(() => void checkHealth(), 30000); + return () => clearInterval(interval); + }, [instances, projectId, repoId]); - const handleRestart = async (instanceId: string) => { - try { - await restartInstance(projectId, repoId, instanceId); - await loadInstances(); - } catch { - setError("Failed to restart instance"); - } - }; + const handleCreate = async () => { + if (!selectedToolType) return; + setError(null); + try { + await createInstance( + projectId, + repoId, + selectedToolType, + displayName || undefined, + ); + setShowCreate(false); + setSelectedToolType(""); + setDisplayName(""); + await loadInstances(); + } catch { + setError("Failed to create instance"); + } + }; - const handleDelete = async (instanceId: string) => { - if (!confirm("Are you sure you want to delete this instance?")) return; - try { - await deleteInstance(projectId, repoId, instanceId); - // Update state immediately instead of reloading - setInstances(prev => prev.filter(i => i.id !== instanceId)); - } catch { - setError("Failed to delete instance"); - } - }; + const handleStart = async (instanceId: string) => { + try { + await startInstance(projectId, repoId, instanceId); + await loadInstances(); + } catch { + setError("Failed to start instance"); + } + }; - const handleRecreateTunnel = async (instanceId: string) => { - try { - await recreateInstanceTunnel(projectId, repoId, instanceId); - await loadInstances(); - } catch { - setError("Failed to recreate tunnel"); - } - }; + const handleStop = async (instanceId: string) => { + try { + await stopInstance(projectId, repoId, instanceId); + setStopConfirmId(null); + await loadInstances(); + } catch { + setError("Failed to stop instance"); + } + }; - const getStatusColor = (status: string) => { - switch (status) { - case "running": - return "var(--success)"; - case "error": - return "var(--danger)"; - case "pending": - case "building": - return "var(--warning)"; - default: - return "var(--muted)"; - } - }; + const handleRestart = async (instanceId: string) => { + try { + await restartInstance(projectId, repoId, instanceId); + await loadInstances(); + } catch { + setError("Failed to restart instance"); + } + }; - const isTunnelUnhealthy = (instance: ToolInstance) => { - if (instance.status !== "running") return false; - if (!instance.url?.startsWith("http")) return false; - const health = healthStatus[instance.id]; - if (!health) return false; - return !health.healthy; - }; + const handleDelete = async (instanceId: string) => { + if (!confirm("Are you sure you want to delete this instance?")) return; + try { + await deleteInstance(projectId, repoId, instanceId); + // Update state immediately instead of reloading + setInstances((prev) => prev.filter((i) => i.id !== instanceId)); + } catch { + setError("Failed to delete instance"); + } + }; - return ( -
-
-

Tool Instances

- -
+ const handleRecreateTunnel = async (instanceId: string) => { + try { + await recreateInstanceTunnel(projectId, repoId, instanceId); + await loadInstances(); + } catch { + setError("Failed to recreate tunnel"); + } + }; - {error && ( -
{error}
- )} + const getStatusColor = (status: string) => { + switch (status) { + case "running": + return "var(--success)"; + case "error": + return "var(--danger)"; + case "pending": + case "building": + return "var(--warning)"; + default: + return "var(--muted)"; + } + }; - {loading ? ( -

Loading instances...

- ) : instances.length === 0 ? ( -

No instances yet. Launch a tool to get started.

- ) : ( -
- {instances.map((instance) => ( -
-
-
{instance.display_name || instance.tool_type_name || "Unnamed Instance"}
-
- - {instance.status} - {isTunnelUnhealthy(instance) && ( - - - tunnel error - - )} -
-
-
- {instance.status === "running" && instance.url && instance.tool_type_interfaces.includes("web") && ( - <> - - - Open - - {isTunnelUnhealthy(instance) && ( - - )} - - )} - {instance.status === "running" && instance.tool_type_interfaces.includes("terminal") && ( - - )} - {instance.status !== "running" && ( - - )} - {instance.status === "running" && ( - <> - {stopConfirmId === instance.id ? ( -
- Stop? - - -
- ) : ( - - )} - - - )} - -
-
- ))} -
- )} + const isTunnelUnhealthy = (instance: ToolInstance) => { + if (instance.status !== "running") return false; + if (!instance.url?.startsWith("http")) return false; + const health = healthStatus[instance.id]; + if (!health) return false; + return !health.healthy; + }; - {showCreate && ( -
-
-

Launch Tool

-
- - -
- - -
-
-
-
- )} -
- ); + return ( +
+
+

Tool Instances

+ +
+ + {error &&
{error}
} + + {loading ? ( +

Loading instances...

+ ) : instances.length === 0 ? ( +

No instances yet. Launch a tool to get started.

+ ) : ( +
+ {instances.map((instance) => ( +
+
+
+ {instance.display_name || + instance.tool_type_name || + "Unnamed Instance"} +
+
+ + {instance.status} + {isTunnelUnhealthy(instance) && ( + + + tunnel error + + )} +
+
+
+ {instance.status === "running" && + instance.url && + instance.tool_type_interfaces.includes("web") && ( + <> + + + Open + + {isTunnelUnhealthy(instance) && ( + + )} + + )} + {instance.status === "running" && + instance.tool_type_interfaces.includes("terminal") && ( + + )} + {instance.status !== "running" && ( + + )} + {instance.status === "running" && ( + <> + {stopConfirmId === instance.id ? ( +
+ Stop? + + +
+ ) : ( + + )} + + + )} + +
+
+ ))} +
+ )} + + {showCreate && ( +
+
+

Launch Tool

+
+ + +
+ + +
+
+
+
+ )} +
+ ); }; diff --git a/apps/web/src/components/repositories-settings-tab.tsx b/apps/web/src/components/repositories-settings-tab.tsx index 4ff6c40..bb6ae65 100644 --- a/apps/web/src/components/repositories-settings-tab.tsx +++ b/apps/web/src/components/repositories-settings-tab.tsx @@ -7,94 +7,95 @@ import { RepositoryCreateDialog } from "./repository-create-dialog"; import { Icon } from "./icon"; export const RepositoriesSettingsTab: React.FC = () => { - const { projectId } = useParams<{ projectId: string }>(); - const [repositories, setRepositories] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreate, setShowCreate] = useState(false); - const [error, setError] = useState(""); + const { projectId } = useParams<{ projectId: string }>(); + const [repositories, setRepositories] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreate, setShowCreate] = useState(false); + const [error, setError] = useState(""); - const loadRepositories = useCallback(async () => { - if (!projectId) { - setLoading(false); - return; - } + const loadRepositories = useCallback(async () => { + if (!projectId) { + setLoading(false); + return; + } - setLoading(true); - try { - const data = await listRepositories(projectId); - setRepositories(data); - } catch { - setError("Failed to load repositories"); - } finally { - setLoading(false); - } - }, [projectId]); + setLoading(true); + try { + const data = await listRepositories(projectId); + setRepositories(data); + } catch { + setError("Failed to load repositories"); + } finally { + setLoading(false); + } + }, [projectId]); - useEffect(() => { - void loadRepositories(); - }, [loadRepositories]); + useEffect(() => { + void loadRepositories(); + }, [loadRepositories]); - const handleDelete = async (repoId: string) => { - if (!projectId) return; - if (!window.confirm("Are you sure you want to delete this repository?")) return; - try { - await deleteRepository(projectId, repoId); - setRepositories((current) => current.filter((r) => r.id !== repoId)); - } catch { - setError("Failed to delete repository"); - } - }; + const handleDelete = async (repoId: string) => { + if (!projectId) return; + if (!window.confirm("Are you sure you want to delete this repository?")) + return; + try { + await deleteRepository(projectId, repoId); + setRepositories((current) => current.filter((r) => r.id !== repoId)); + } catch { + setError("Failed to delete repository"); + } + }; - if (loading) return
Loading...
; + if (loading) return
Loading...
; - return ( -
-
-

Repositories

- -
- {error &&
{error}
} + return ( +
+
+

Repositories

+ +
+ {error &&
{error}
} -
- {repositories.length === 0 ? ( -

No repositories yet.

- ) : ( - repositories.map((repo) => ( -
-
-

{repo.name}

-

{repo.remote_url}

- - {repo.is_mirror ? "Mirror" : "Clone"} - -
- -
- )) - )} -
+
+ {repositories.length === 0 ? ( +

No repositories yet.

+ ) : ( + repositories.map((repo) => ( +
+
+

{repo.name}

+

{repo.remote_url}

+ + {repo.is_mirror ? "Mirror" : "Clone"} + +
+ +
+ )) + )} +
- {showCreate && ( - setShowCreate(false)} - onCreated={loadRepositories} - /> - )} -
- ); + {showCreate && ( + setShowCreate(false)} + onCreated={loadRepositories} + /> + )} +
+ ); }; diff --git a/apps/web/src/components/repository-create-dialog.tsx b/apps/web/src/components/repository-create-dialog.tsx index a88a5a6..12e0c34 100644 --- a/apps/web/src/components/repository-create-dialog.tsx +++ b/apps/web/src/components/repository-create-dialog.tsx @@ -1,294 +1,326 @@ import { useEffect, useRef, useState } from "react"; -import type { GitRepositoryCreate, URLParseResult } from "../types/git-repository"; +import type { + GitRepositoryCreate, + URLParseResult, +} from "../types/git-repository"; import { createRepository, parseGitUrl } from "../api/git_repositories"; import { Icon } from "./icon"; type CreateMode = "clone" | "blank"; -type UrlValidationStatus = "idle" | "validating" | "valid" | "needs-parsing" | "invalid"; +type UrlValidationStatus = + | "idle" + | "validating" + | "valid" + | "needs-parsing" + | "invalid"; interface RepositoryCreateDialogProps { - projectId: string; - open: boolean; - title: string; - onClose: () => void; - onCreated: () => Promise | void; + projectId: string; + open: boolean; + title: string; + onClose: () => void; + onCreated: () => Promise | void; } -export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCreated }: RepositoryCreateDialogProps) => { - const [createMode, setCreateMode] = useState("clone"); - const [formName, setFormName] = useState(""); - const [owner, setOwner] = useState(""); - const [repoName, setRepoName] = useState(""); - const [advancedUrl, setAdvancedUrl] = useState(""); - const [useAdvancedUrl, setUseAdvancedUrl] = useState(false); - const [formError, setFormError] = useState(null); - const [urlValidation, setUrlValidation] = useState<{ - status: UrlValidationStatus; - result: URLParseResult | null; - }>({ status: "idle", result: null }); - const debounceTimer = useRef | null>(null); +export const RepositoryCreateDialog = ({ + projectId, + open, + title, + onClose, + onCreated, +}: RepositoryCreateDialogProps) => { + const [createMode, setCreateMode] = useState("clone"); + const [formName, setFormName] = useState(""); + const [owner, setOwner] = useState(""); + const [repoName, setRepoName] = useState(""); + const [advancedUrl, setAdvancedUrl] = useState(""); + const [useAdvancedUrl, setUseAdvancedUrl] = useState(false); + const [formError, setFormError] = useState(null); + const [urlValidation, setUrlValidation] = useState<{ + status: UrlValidationStatus; + result: URLParseResult | null; + }>({ status: "idle", result: null }); + const debounceTimer = useRef | null>(null); - useEffect(() => { - if (!open && debounceTimer.current) { - clearTimeout(debounceTimer.current); - debounceTimer.current = null; - } - }, [open]); + useEffect(() => { + if (!open && debounceTimer.current) { + clearTimeout(debounceTimer.current); + debounceTimer.current = null; + } + }, [open]); - useEffect(() => { - if (!open) return; - if (!useAdvancedUrl) { - setUrlValidation({ status: "idle", result: null }); - return; - } + useEffect(() => { + if (!open) return; + if (!useAdvancedUrl) { + setUrlValidation({ status: "idle", result: null }); + return; + } - if (debounceTimer.current) { - clearTimeout(debounceTimer.current); - } + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } - if (!advancedUrl.trim()) { - setUrlValidation({ status: "idle", result: null }); - return; - } + if (!advancedUrl.trim()) { + setUrlValidation({ status: "idle", result: null }); + return; + } - setUrlValidation({ status: "validating", result: null }); + setUrlValidation({ status: "validating", result: null }); - debounceTimer.current = setTimeout(async () => { - try { - const result = await parseGitUrl(advancedUrl.trim()); - if (result.is_valid_clone_url) { - setUrlValidation({ status: "valid", result }); - } else if (result.needs_parsing) { - setUrlValidation({ status: "needs-parsing", result }); - } else { - setUrlValidation({ status: "invalid", result }); - } - } catch { - setUrlValidation({ status: "invalid", result: null }); - } - }, 300); + debounceTimer.current = setTimeout(async () => { + try { + const result = await parseGitUrl(advancedUrl.trim()); + if (result.is_valid_clone_url) { + setUrlValidation({ status: "valid", result }); + } else if (result.needs_parsing) { + setUrlValidation({ status: "needs-parsing", result }); + } else { + setUrlValidation({ status: "invalid", result }); + } + } catch { + setUrlValidation({ status: "invalid", result: null }); + } + }, 300); - return () => { - if (debounceTimer.current) { - clearTimeout(debounceTimer.current); - } - }; - }, [advancedUrl, open, useAdvancedUrl]); + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current); + } + }; + }, [advancedUrl, open, useAdvancedUrl]); - const resetForm = () => { - setCreateMode("clone"); - setFormName(""); - setOwner(""); - setRepoName(""); - setAdvancedUrl(""); - setUseAdvancedUrl(false); - setFormError(null); - setUrlValidation({ status: "idle", result: null }); - }; + const resetForm = () => { + setCreateMode("clone"); + setFormName(""); + setOwner(""); + setRepoName(""); + setAdvancedUrl(""); + setUseAdvancedUrl(false); + setFormError(null); + setUrlValidation({ status: "idle", result: null }); + }; - const handleClose = () => { - resetForm(); - onClose(); - }; + const handleClose = () => { + resetForm(); + onClose(); + }; - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - setFormError(null); + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + setFormError(null); - if (!formName.trim()) { - setFormError("Repository name is required"); - return; - } + if (!formName.trim()) { + setFormError("Repository name is required"); + return; + } - try { - const input: GitRepositoryCreate = { - name: formName.trim(), - remote_url: undefined, - }; + try { + const input: GitRepositoryCreate = { + name: formName.trim(), + remote_url: undefined, + }; - if (createMode === "clone") { - if (useAdvancedUrl) { - if (!advancedUrl.trim()) { - setFormError("Remote URL is required for advanced cloning"); - return; - } - input.remote_url = advancedUrl.trim(); - } else { - if (!owner.trim() || !repoName.trim()) { - setFormError("Owner and repository name are required"); - return; - } - input.remote_url = `git@git.commumedia.org:${owner.trim()}/${repoName.trim()}.git`; - } - } + if (createMode === "clone") { + if (useAdvancedUrl) { + if (!advancedUrl.trim()) { + setFormError("Remote URL is required for advanced cloning"); + return; + } + input.remote_url = advancedUrl.trim(); + } else { + if (!owner.trim() || !repoName.trim()) { + setFormError("Owner and repository name are required"); + return; + } + input.remote_url = `git@git.commumedia.org:${owner.trim()}/${repoName.trim()}.git`; + } + } - await createRepository(projectId, input); - handleClose(); - await onCreated(); - } catch (error: unknown) { - const response = error as { response?: { data?: { detail?: string } } }; - const detail = response.response?.data?.detail; - setFormError(typeof detail === "string" ? detail : "Failed to create repository"); - } - }; + await createRepository(projectId, input); + handleClose(); + await onCreated(); + } catch (error: unknown) { + const response = error as { response?: { data?: { detail?: string } } }; + const detail = response.response?.data?.detail; + setFormError( + typeof detail === "string" ? detail : "Failed to create repository", + ); + } + }; - const handleUseSuggestedUrl = () => { - if (urlValidation.result?.base_url) { - setAdvancedUrl(urlValidation.result.base_url); - setUrlValidation({ status: "idle", result: null }); - setFormError(null); - } - }; + const handleUseSuggestedUrl = () => { + if (urlValidation.result?.base_url) { + setAdvancedUrl(urlValidation.result.base_url); + setUrlValidation({ status: "idle", result: null }); + setFormError(null); + } + }; - const getUrlInputClass = () => { - switch (urlValidation.status) { - case "valid": - return "valid-url"; - case "needs-parsing": - return "needs-parsing-url"; - case "invalid": - return "invalid-url"; - default: - return ""; - } - }; + const getUrlInputClass = () => { + switch (urlValidation.status) { + case "valid": + return "valid-url"; + case "needs-parsing": + return "needs-parsing-url"; + case "invalid": + return "invalid-url"; + default: + return ""; + } + }; - if (!open) return null; + if (!open) return null; - return ( -
-
-

{title}

-

- Clone an existing repository from git.commumedia.org, or create a blank bare repo here. -

-
-
- - -
- - {createMode === "clone" && !useAdvancedUrl && ( - <> - - -

SSH target: git@git.commumedia.org:{owner || "owner"}/{repoName || "repo"}.git

- - - )} - {createMode === "clone" && useAdvancedUrl && ( - - )} - {formError && ( -
-

{formError}

-
- )} -
- - -
-
-
-
- ); + return ( +
+
+

{title}

+

+ Clone an existing repository from git.commumedia.org, or create a + blank bare repo here. +

+
+
+ + +
+ + {createMode === "clone" && !useAdvancedUrl && ( + <> + + +

+ SSH target: git@git.commumedia.org:{owner || "owner"}/ + {repoName || "repo"}.git +

+ + + )} + {createMode === "clone" && useAdvancedUrl && ( + + )} + {formError && ( +
+

{formError}

+
+ )} +
+ + +
+
+
+
+ ); }; diff --git a/apps/web/src/pages/dashboard.tsx b/apps/web/src/pages/dashboard.tsx index 7ddc70b..48b324d 100644 --- a/apps/web/src/pages/dashboard.tsx +++ b/apps/web/src/pages/dashboard.tsx @@ -2,7 +2,14 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { getDashboardSummary, type DashboardSummary } from "../api/dashboard"; -import { createInstance, getUserSessions, startInstance, stopInstance, deleteInstance, recreateInstanceTunnel } from "../api/sessions"; +import { + createInstance, + getUserSessions, + startInstance, + stopInstance, + deleteInstance, + recreateInstanceTunnel, +} from "../api/sessions"; import { listProjects } from "../api/projects"; import { listRepositories } from "../api/git_repositories"; import { listToolTypes } from "../api/tool_types"; @@ -16,326 +23,461 @@ import { Icon } from "../components/icon"; type HomeStatus = "loading" | "ready" | "error"; const summaryCards = [ - { label: "Open sessions", key: "openSessions" }, - { label: "Projects", key: "projects" }, - { label: "Repositories", key: "repositories" }, + { label: "Open sessions", key: "openSessions" }, + { label: "Projects", key: "projects" }, + { label: "Repositories", key: "repositories" }, ] as const; type SessionView = SessionApi; export const HomePage = () => { - const navigate = useNavigate(); - const [status, setStatus] = useState("loading"); - const [summary, setSummary] = useState(null); - const [sessions, setSessions] = useState([]); - const [projects, setProjects] = useState([]); - const [repositories, setRepositories] = useState([]); - const [toolTypes, setToolTypes] = useState([]); - const [selectedProject, setSelectedProject] = useState(""); - const [selectedRepo, setSelectedRepo] = useState(""); - const [selectedToolType, setSelectedToolType] = useState(""); - const [displayName, setDisplayName] = useState(""); - const [saveState, setSaveState] = useState<"idle" | "saving" | "error">("idle"); - const [actionBusy, setActionBusy] = useState(null); - const safeSessions = Array.isArray(sessions) ? sessions : []; + const navigate = useNavigate(); + const [status, setStatus] = useState("loading"); + const [summary, setSummary] = useState(null); + const [sessions, setSessions] = useState([]); + const [projects, setProjects] = useState([]); + const [repositories, setRepositories] = useState([]); + const [toolTypes, setToolTypes] = useState([]); + const [selectedProject, setSelectedProject] = useState(""); + const [selectedRepo, setSelectedRepo] = useState(""); + const [selectedToolType, setSelectedToolType] = useState(""); + const [displayName, setDisplayName] = useState(""); + const [saveState, setSaveState] = useState<"idle" | "saving" | "error">( + "idle", + ); + const [actionBusy, setActionBusy] = useState(null); + const safeSessions = Array.isArray(sessions) ? sessions : []; - const loadHome = useCallback(async () => { - setStatus("loading"); - try { - const [dashboard, sessionData, projectData, toolTypeData] = await Promise.all([ - getDashboardSummary(), - getUserSessions(), - listProjects(), - listToolTypes(), - ]); - setSummary(dashboard); - setSessions(sessionData as SessionView[]); - setProjects(projectData); - setToolTypes(toolTypeData); - setStatus("ready"); - } catch { - setStatus("error"); - } - }, []); + const loadHome = useCallback(async () => { + setStatus("loading"); + try { + const [dashboard, sessionData, projectData, toolTypeData] = + await Promise.all([ + getDashboardSummary(), + getUserSessions(), + listProjects(), + listToolTypes(), + ]); + setSummary(dashboard); + setSessions(sessionData as SessionView[]); + setProjects(projectData); + setToolTypes(toolTypeData); + setStatus("ready"); + } catch { + setStatus("error"); + } + }, []); - useEffect(() => { - void loadHome(); - }, [loadHome]); + useEffect(() => { + void loadHome(); + }, [loadHome]); - useEffect(() => { - if (!selectedProject) { - setRepositories([]); - return; - } + useEffect(() => { + if (!selectedProject) { + setRepositories([]); + return; + } - const loadRepos = async () => { - try { - const data = await listRepositories(selectedProject); - setRepositories(data); - } catch { - setRepositories([]); - } - }; + const loadRepos = async () => { + try { + const data = await listRepositories(selectedProject); + setRepositories(data); + } catch { + setRepositories([]); + } + }; - void loadRepos(); - }, [selectedProject]); + void loadRepos(); + }, [selectedProject]); - const activeSessions = useMemo( - () => safeSessions.filter((session) => ["running", "building", "pending"].includes(session.status)), - [safeSessions] - ); + const activeSessions = useMemo( + () => + safeSessions.filter((session) => + ["running", "building", "pending"].includes(session.status), + ), + [safeSessions], + ); - const recentSessions = useMemo( - () => safeSessions.filter((session) => ["stopped", "error"].includes(session.status)).slice(0, 5), - [safeSessions] - ); + const recentSessions = useMemo( + () => + safeSessions + .filter((session) => ["stopped", "error"].includes(session.status)) + .slice(0, 5), + [safeSessions], + ); - const handleCreate = async (event: React.FormEvent) => { - event.preventDefault(); - if (!selectedProject || !selectedRepo || !selectedToolType) return; + const handleCreate = async (event: React.FormEvent) => { + event.preventDefault(); + if (!selectedProject || !selectedRepo || !selectedToolType) return; - setSaveState("saving"); - try { - const instance = await createInstance(selectedProject, selectedRepo, selectedToolType, displayName || undefined); - await startInstance(selectedProject, selectedRepo, instance.id); - await updateUserConfig({ last_session_id: instance.id }); - setDisplayName(""); - setSelectedProject(""); - setSelectedRepo(""); - setSelectedToolType(""); - setSaveState("idle"); - await loadHome(); - } catch { - setSaveState("error"); - } - }; + setSaveState("saving"); + try { + const instance = await createInstance( + selectedProject, + selectedRepo, + selectedToolType, + displayName || undefined, + ); + await startInstance(selectedProject, selectedRepo, instance.id); + await updateUserConfig({ last_session_id: instance.id }); + setDisplayName(""); + setSelectedProject(""); + setSelectedRepo(""); + setSelectedToolType(""); + setSaveState("idle"); + await loadHome(); + } catch { + setSaveState("error"); + } + }; - const handleOpen = (session: SessionView) => { - if (session.url) { - window.open(session.url, "_blank", "noopener,noreferrer"); - return; - } - if (session.tool_type_interfaces.includes("terminal")) { - navigate(`/instances/${session.id}/terminal`); - return; - } - navigate(`/projects/${session.project_id}`); - }; + const handleOpen = (session: SessionView) => { + if (session.url) { + window.open(session.url, "_blank", "noopener,noreferrer"); + return; + } + if (session.tool_type_interfaces.includes("terminal")) { + navigate(`/instances/${session.id}/terminal`); + return; + } + navigate(`/projects/${session.project_id}`); + }; - const handleStop = async (session: SessionView) => { - setActionBusy(session.id); - try { - await stopInstance(session.project_id, session.repository_id, session.id); - await loadHome(); - } finally { - setActionBusy(null); - } - }; + const handleStop = async (session: SessionView) => { + setActionBusy(session.id); + try { + await stopInstance(session.project_id, session.repository_id, session.id); + await loadHome(); + } finally { + setActionBusy(null); + } + }; - const handleDelete = async (session: SessionView) => { - setActionBusy(session.id); - try { - await deleteInstance(session.project_id, session.repository_id, session.id); - await loadHome(); - } finally { - setActionBusy(null); - } - }; + const handleDelete = async (session: SessionView) => { + setActionBusy(session.id); + try { + await deleteInstance( + session.project_id, + session.repository_id, + session.id, + ); + await loadHome(); + } finally { + setActionBusy(null); + } + }; - const handleRecreateTunnel = async (session: SessionView) => { - setActionBusy(session.id); - try { - await recreateInstanceTunnel(session.project_id, session.repository_id, session.id); - await loadHome(); - } finally { - setActionBusy(null); - } - }; + const handleRecreateTunnel = async (session: SessionView) => { + setActionBusy(session.id); + try { + await recreateInstanceTunnel( + session.project_id, + session.repository_id, + session.id, + ); + await loadHome(); + } finally { + setActionBusy(null); + } + }; - return ( -
-
-
-

Workspace overview

-

Home

-

Open sessions, available projects, and the fastest path back into work.

-
-
- - -
-
+ return ( +
+
+
+

Workspace overview

+

Home

+

+ Open sessions, available projects, and the fastest path back into + work. +

+
+
+ + +
+
- {status === "loading" &&

Loading overview...

} + {status === "loading" &&

Loading overview...

} - {status === "error" && ( -
-

Unable to load your workspace overview.

- -
- )} + {status === "error" && ( +
+

Unable to load your workspace overview.

+ +
+ )} - {status === "ready" && summary && ( - <> -
- {summaryCards.map((card) => ( -
-

{card.label}

-

- {card.key === "openSessions" - ? activeSessions.length - : card.key === "projects" - ? summary.projects - : summary.repositories} -

-
- ))} -
+ {status === "ready" && summary && ( + <> +
+ {summaryCards.map((card) => ( +
+

{card.label}

+

+ {card.key === "openSessions" + ? activeSessions.length + : card.key === "projects" + ? summary.projects + : summary.repositories} +

+
+ ))} +
-
-
-
-

Open sessions

-

{activeSessions.length}

-
-
- {activeSessions.length === 0 ? ( -

No active sessions right now.

- ) : ( -
- {activeSessions.map((session) => ( -
-
-
-

{session.display_name}

- {session.status} -
-

{session.project_name} · {session.repository_name}

-

{session.tool_type_name}

-
-
- - - - -
-
- ))} -
- )} -
+
+
+
+

Open sessions

+

{activeSessions.length}

+
+
+ {activeSessions.length === 0 ? ( +

No active sessions right now.

+ ) : ( +
+ {activeSessions.map((session) => ( +
+
+
+

{session.display_name}

+ + {session.status} + +
+

+ {session.project_name} · {session.repository_name} +

+

{session.tool_type_name}

+
+
+ + + + +
+
+ ))} +
+ )} +
-
-
-
-

Available projects

-

{projects.length}

-
- -
- {projects.length === 0 ? ( -

No projects yet.

- ) : ( -
- {projects.map((project) => ( -
-
-

{project.name}

- {project.description &&

{project.description}

} -
- -
- ))} -
- )} -
+
+
+
+

Available projects

+

{projects.length}

+
+ +
+ {projects.length === 0 ? ( +

No projects yet.

+ ) : ( +
+ {projects.map((project) => ( +
+
+

{project.name}

+ {project.description && ( +

{project.description}

+ )} +
+ +
+ ))} +
+ )} +
-
-
-
-

Quick create

-

Start a session

-
-
-
-
- - - -
- -
- - {saveState === "error" && Failed to create session} -
-
-
+
+
+
+

Quick create

+

Start a session

+
+
+
+
+ + + +
+ +
+ + {saveState === "error" && ( + Failed to create session + )} +
+
+
- {recentSessions.length > 0 && ( -
-
-
-

Recent sessions

-

{recentSessions.length}

-
-
-
- {recentSessions.map((session) => ( -
-
- {session.display_name} - {session.project_name} · {session.tool_type_name} -
- -
- ))} -
-
- )} - - )} -
- ); + {recentSessions.length > 0 && ( +
+
+
+

Recent sessions

+

{recentSessions.length}

+
+
+
+ {recentSessions.map((session) => ( +
+
+ + {session.display_name} + + + {session.project_name} · {session.tool_type_name} + +
+ +
+ ))} +
+
+ )} + + )} +
+ ); }; export { HomePage as DashboardPage }; diff --git a/apps/web/src/pages/git-repositories.tsx b/apps/web/src/pages/git-repositories.tsx index b4a86e3..c8d4487 100644 --- a/apps/web/src/pages/git-repositories.tsx +++ b/apps/web/src/pages/git-repositories.tsx @@ -2,138 +2,151 @@ import { useCallback, useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import type { GitRepository } from "../types/git-repository"; -import { - deleteRepository, - listRepositories, -} from "../api/git_repositories"; +import { deleteRepository, listRepositories } from "../api/git_repositories"; import { Icon } from "../components/icon"; import { RepositoryCreateDialog } from "../components/repository-create-dialog"; type RepoStatus = "loading" | "ready" | "error"; export const GitRepositoriesPage = () => { - const { projectId } = useParams<{ projectId: string }>(); - const navigate = useNavigate(); - const [status, setStatus] = useState("loading"); - const [repositories, setRepositories] = useState([]); - const [showCreate, setShowCreate] = useState(false); - const [deleteConfirmId, setDeleteConfirmId] = useState(null); + const { projectId } = useParams<{ projectId: string }>(); + const navigate = useNavigate(); + const [status, setStatus] = useState("loading"); + const [repositories, setRepositories] = useState([]); + const [showCreate, setShowCreate] = useState(false); + const [deleteConfirmId, setDeleteConfirmId] = useState(null); - const loadRepositories = useCallback(async () => { - if (!projectId) return; - setStatus("loading"); - try { - const data = await listRepositories(projectId); - setRepositories(data); - setStatus("ready"); - } catch { - setRepositories([]); - setStatus("error"); - } - }, [projectId]); + const loadRepositories = useCallback(async () => { + if (!projectId) return; + setStatus("loading"); + try { + const data = await listRepositories(projectId); + setRepositories(data); + setStatus("ready"); + } catch { + setRepositories([]); + setStatus("error"); + } + }, [projectId]); - useEffect(() => { - void loadRepositories(); - }, [loadRepositories]); + useEffect(() => { + void loadRepositories(); + }, [loadRepositories]); - const handleDelete = async (repoId: string) => { - if (!projectId) return; - try { - await deleteRepository(projectId, repoId); - setDeleteConfirmId(null); - await loadRepositories(); - } catch { - setDeleteConfirmId(null); - } - }; + const handleDelete = async (repoId: string) => { + if (!projectId) return; + try { + await deleteRepository(projectId, repoId); + setDeleteConfirmId(null); + await loadRepositories(); + } catch { + setDeleteConfirmId(null); + } + }; - const isEmpty = status === "ready" && repositories.length === 0; + const isEmpty = status === "ready" && repositories.length === 0; - return ( -
-
-

Repositories

- -
+ return ( +
+
+

Repositories

+ +
- {status === "loading" &&

Loading repositories...

} + {status === "loading" &&

Loading repositories...

} - {status === "error" && ( -
-

Failed to load repositories

- -
- )} + {status === "error" && ( +
+

Failed to load repositories

+ +
+ )} - {isEmpty &&

No repositories yet. Create your first repository above.

} + {isEmpty && ( +

+ No repositories yet. Create your first repository above. +

+ )} - {status === "ready" && repositories.length > 0 && ( -
- {repositories.map((repo) => ( -
-
-

{repo.name}

- {repo.is_mirror && repo.remote_url && ( -

Mirror of {repo.remote_url}

- )} -

{repo.path}

-
-
- - {deleteConfirmId === repo.id ? ( -
- Are you sure? - - -
- ) : ( - - )} -
-
- ))} -
- )} + {status === "ready" && repositories.length > 0 && ( +
+ {repositories.map((repo) => ( +
+
+

{repo.name}

+ {repo.is_mirror && repo.remote_url && ( +

Mirror of {repo.remote_url}

+ )} +

{repo.path}

+
+
+ + {deleteConfirmId === repo.id ? ( +
+ Are you sure? + + +
+ ) : ( + + )} +
+
+ ))} +
+ )} - {showCreate && ( - setShowCreate(false)} - onCreated={loadRepositories} - /> - )} -
- ); + {showCreate && ( + setShowCreate(false)} + onCreated={loadRepositories} + /> + )} +
+ ); }; diff --git a/apps/web/src/pages/repo-workspace.tsx b/apps/web/src/pages/repo-workspace.tsx index 0f2f653..42688d5 100644 --- a/apps/web/src/pages/repo-workspace.tsx +++ b/apps/web/src/pages/repo-workspace.tsx @@ -7,9 +7,9 @@ import { apiClient } from "../api/client"; import type { GitRepository } from "../types/git-repository"; import type { ToolType } from "../types/tool-type"; import { - getRepositoryStatus, - listRepositories, - type GitStatus, + getRepositoryStatus, + listRepositories, + type GitStatus, } from "../api/git_repositories"; import { CommitPanel } from "../components/commit-panel"; import { FileEditor } from "../components/file-editor"; @@ -21,374 +21,382 @@ import { listToolTypes } from "../api/tool_types"; type WorkspaceStatus = "loading" | "ready" | "error" | "empty"; interface FileTreeEntry { - name: string; - type: "file" | "directory"; - path: string; - size?: number; - mode?: string; - last_commit?: { - hash: string; - message: string; - author: string; - date: string; - } | null; + name: string; + type: "file" | "directory"; + path: string; + size?: number; + mode?: string; + last_commit?: { + hash: string; + message: string; + author: string; + date: string; + } | null; } interface Project { - id: string; - name: string; - description?: string | null; + id: string; + name: string; + description?: string | null; } export const RepoWorkspace = () => { - const { projectId } = useParams<{ projectId: string }>(); - const [searchParams, setSearchParams] = useSearchParams(); + const { projectId } = useParams<{ projectId: string }>(); + const [searchParams, setSearchParams] = useSearchParams(); - const [status, setStatus] = useState("loading"); - const [project, setProject] = useState(null); - const [repositories, setRepositories] = useState([]); - const [selectedRepoId, setSelectedRepoId] = useState( - searchParams.get("repo") - ); - const [branches, setBranches] = useState([]); - const [currentBranch, setCurrentBranch] = useState("main"); - const [gitStatus, setGitStatus] = useState(null); - const [toolTypes, setToolTypes] = useState([]); + const [status, setStatus] = useState("loading"); + const [project, setProject] = useState(null); + const [repositories, setRepositories] = useState([]); + const [selectedRepoId, setSelectedRepoId] = useState( + searchParams.get("repo"), + ); + const [branches, setBranches] = useState([]); + const [currentBranch, setCurrentBranch] = useState("main"); + const [gitStatus, setGitStatus] = useState(null); + const [toolTypes, setToolTypes] = useState([]); - const loadProject = useCallback(async () => { - if (!projectId) return; - try { - const response = await apiClient.get(`/projects/${projectId}`); - setProject(response.data); - } catch { - setProject(null); - } - }, [projectId]); + const loadProject = useCallback(async () => { + if (!projectId) return; + try { + const response = await apiClient.get(`/projects/${projectId}`); + setProject(response.data); + } catch { + setProject(null); + } + }, [projectId]); - const loadRepositories = useCallback(async () => { - if (!projectId) return; + const loadRepositories = useCallback(async () => { + if (!projectId) return; - setStatus("loading"); - try { - const data = await listRepositories(projectId); - setRepositories(data); + setStatus("loading"); + try { + const data = await listRepositories(projectId); + setRepositories(data); - if (data.length === 0) { - setStatus("empty"); - } else { - setStatus("ready"); - // If no repo selected, select the first one - if (!selectedRepoId) { - setSelectedRepoId(data[0].id); - const newParams = new URLSearchParams(searchParams); - newParams.set("repo", data[0].id); - setSearchParams(newParams, { replace: true }); - } - } - } catch { - setRepositories([]); - setStatus("error"); - } - }, [projectId, selectedRepoId, searchParams, setSearchParams]); + if (data.length === 0) { + setStatus("empty"); + } else { + setStatus("ready"); + // If no repo selected, select the first one + if (!selectedRepoId) { + setSelectedRepoId(data[0].id); + const newParams = new URLSearchParams(searchParams); + newParams.set("repo", data[0].id); + setSearchParams(newParams, { replace: true }); + } + } + } catch { + setRepositories([]); + setStatus("error"); + } + }, [projectId, selectedRepoId, searchParams, setSearchParams]); - const loadBranches = useCallback(async () => { - if (!projectId || !selectedRepoId) return; - try { - const response = await apiClient.get( - `/projects/${projectId}/repositories/${selectedRepoId}/branches` - ); - const branchList = response.data.branches.map((b: { name: string }) => b.name); - setBranches(branchList); - const defaultBranch = response.data.default_branch; - if (defaultBranch) { - setCurrentBranch(defaultBranch); - } - } catch { - setBranches([]); - } - }, [projectId, selectedRepoId]); + const loadBranches = useCallback(async () => { + if (!projectId || !selectedRepoId) return; + try { + const response = await apiClient.get( + `/projects/${projectId}/repositories/${selectedRepoId}/branches`, + ); + const branchList = response.data.branches.map( + (b: { name: string }) => b.name, + ); + setBranches(branchList); + const defaultBranch = response.data.default_branch; + if (defaultBranch) { + setCurrentBranch(defaultBranch); + } + } catch { + setBranches([]); + } + }, [projectId, selectedRepoId]); - const loadGitStatus = useCallback(async () => { - if (!projectId || !selectedRepoId) return; - try { - const data = await getRepositoryStatus(projectId, selectedRepoId); - setGitStatus(data); - } catch { - setGitStatus(null); - } - }, [projectId, selectedRepoId]); + const loadGitStatus = useCallback(async () => { + if (!projectId || !selectedRepoId) return; + try { + const data = await getRepositoryStatus(projectId, selectedRepoId); + setGitStatus(data); + } catch { + setGitStatus(null); + } + }, [projectId, selectedRepoId]); - const loadToolTypes = useCallback(async () => { - try { - const data = await listToolTypes(); - setToolTypes(data); - } catch { - setToolTypes([]); - } - }, []); + const loadToolTypes = useCallback(async () => { + try { + const data = await listToolTypes(); + setToolTypes(data); + } catch { + setToolTypes([]); + } + }, []); - useEffect(() => { - void loadProject(); - void loadRepositories(); - void loadToolTypes(); - }, [loadProject, loadRepositories, loadToolTypes]); + useEffect(() => { + void loadProject(); + void loadRepositories(); + void loadToolTypes(); + }, [loadProject, loadRepositories, loadToolTypes]); - useEffect(() => { - void loadBranches(); - void loadGitStatus(); - }, [loadBranches, loadGitStatus]); + useEffect(() => { + void loadBranches(); + void loadGitStatus(); + }, [loadBranches, loadGitStatus]); - const handleRepoChange = (repoId: string) => { - setSelectedRepoId(repoId); - const newParams = new URLSearchParams(searchParams); - newParams.set("repo", repoId); - newParams.delete("branch"); - newParams.delete("path"); - setSearchParams(newParams); - }; + const handleRepoChange = (repoId: string) => { + setSelectedRepoId(repoId); + const newParams = new URLSearchParams(searchParams); + newParams.set("repo", repoId); + newParams.delete("branch"); + newParams.delete("path"); + setSearchParams(newParams); + }; - const selectedRepo = repositories.find((r) => r.id === selectedRepoId); + const selectedRepo = repositories.find((r) => r.id === selectedRepoId); - return ( -
- {project && ( - - )} + return ( +
+ {project && ( + + )} - {status === "loading" && ( -

Loading repositories...

- )} + {status === "loading" &&

Loading repositories...

} - {status === "error" && ( -
-

Failed to load repositories

- -
- )} + {status === "error" && ( +
+

Failed to load repositories

+ +
+ )} - {status === "empty" && ( -
-

No repositories in this project yet.

- - Manage Repositories - -
- )} + {status === "empty" && ( +
+

No repositories in this project yet.

+ + Manage Repositories + +
+ )} - {status === "ready" && repositories.length > 0 && ( - <> - {selectedRepoId && ( - { - setCurrentBranch(branch); - const newParams = new URLSearchParams(searchParams); - newParams.set("branch", branch); - setSearchParams(newParams); - }} - onRefresh={() => { - void loadBranches(); - void loadGitStatus(); - window.dispatchEvent(new CustomEvent("refresh-file-tree")); - }} - /> - )} -
- -
- {selectedRepoId && ( - - )} -
-
- - )} -
- ); +
+ {selectedRepoId && ( + + )} +
+
+ + )} + + ); }; // File Browser Component const FileBrowser = ({ - projectId, - repoId, - gitStatus, + projectId, + repoId, + gitStatus, }: { - projectId: string; - repoId: string; - gitStatus: GitStatus | null; + projectId: string; + repoId: string; + gitStatus: GitStatus | null; }) => { - const [searchParams, setSearchParams] = useSearchParams(); - const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [searchParams, setSearchParams] = useSearchParams(); + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const branch = searchParams.get("branch") || "main"; - const path = searchParams.get("path") || ""; + const branch = searchParams.get("branch") || "main"; + const path = searchParams.get("path") || ""; - const loadFiles = useCallback(async () => { - setLoading(true); - setError(null); - try { - const response = await apiClient.get( - `/projects/${projectId}/repositories/${repoId}/files`, - { - params: { - branch, - path, - }, - } - ); - setEntries(response.data.entries || []); - } catch { - setError("Failed to load files"); - } finally { - setLoading(false); - } - }, [projectId, repoId, branch, path]); + const loadFiles = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await apiClient.get( + `/projects/${projectId}/repositories/${repoId}/files`, + { + params: { + branch, + path, + }, + }, + ); + setEntries(response.data.entries || []); + } catch { + setError("Failed to load files"); + } finally { + setLoading(false); + } + }, [projectId, repoId, branch, path]); - useEffect(() => { - void loadFiles(); - }, [loadFiles]); + useEffect(() => { + void loadFiles(); + }, [loadFiles]); - // Listen for refresh events - useEffect(() => { - const handleRefresh = () => void loadFiles(); - window.addEventListener("refresh-file-tree", handleRefresh); - return () => window.removeEventListener("refresh-file-tree", handleRefresh); - }, [loadFiles]); + // Listen for refresh events + useEffect(() => { + const handleRefresh = () => void loadFiles(); + window.addEventListener("refresh-file-tree", handleRefresh); + return () => window.removeEventListener("refresh-file-tree", handleRefresh); + }, [loadFiles]); - const handleEntryClick = (entry: FileTreeEntry) => { - if (entry.type === "directory") { - const newParams = new URLSearchParams(searchParams); - newParams.set("path", entry.path); - setSearchParams(newParams); - } else { - const newParams = new URLSearchParams(searchParams); - newParams.set("file", entry.path); - setSearchParams(newParams); - } - }; + const handleEntryClick = (entry: FileTreeEntry) => { + if (entry.type === "directory") { + const newParams = new URLSearchParams(searchParams); + newParams.set("path", entry.path); + setSearchParams(newParams); + } else { + const newParams = new URLSearchParams(searchParams); + newParams.set("file", entry.path); + setSearchParams(newParams); + } + }; - const navigateUp = () => { - if (!path) return; - const parentPath = path.split("/").slice(0, -1).join("/"); - const newParams = new URLSearchParams(searchParams); - if (parentPath) { - newParams.set("path", parentPath); - } else { - newParams.delete("path"); - } - setSearchParams(newParams); - }; + const navigateUp = () => { + if (!path) return; + const parentPath = path.split("/").slice(0, -1).join("/"); + const newParams = new URLSearchParams(searchParams); + if (parentPath) { + newParams.set("path", parentPath); + } else { + newParams.delete("path"); + } + setSearchParams(newParams); + }; - const getFileStatus = (filePath: string): string | null => { - if (!gitStatus) return null; - if (gitStatus.modified.includes(filePath)) return "modified"; - if (gitStatus.added.includes(filePath)) return "added"; - if (gitStatus.deleted.includes(filePath)) return "deleted"; - if (gitStatus.untracked.includes(filePath)) return "untracked"; - return null; - }; + const getFileStatus = (filePath: string): string | null => { + if (!gitStatus) return null; + if (gitStatus.modified.includes(filePath)) return "modified"; + if (gitStatus.added.includes(filePath)) return "added"; + if (gitStatus.deleted.includes(filePath)) return "deleted"; + if (gitStatus.untracked.includes(filePath)) return "untracked"; + return null; + }; - if (loading) return

Loading files...

; - if (error) return

{error}

; + if (loading) return

Loading files...

; + if (error) return

{error}

; - return ( -
- {path && ( - - )} - {entries.length === 0 && ( -

No files in this repository yet.

- )} - {entries.map((entry) => { - const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null; - return ( - - ); - })} -
- ); + return ( +
+ {path && ( + + )} + {entries.length === 0 && ( +

No files in this repository yet.

+ )} + {entries.map((entry) => { + const fileStatus = + entry.type === "file" ? getFileStatus(entry.path) : null; + return ( + + ); + })} +
+ ); }; diff --git a/apps/web/src/pages/sessions.tsx b/apps/web/src/pages/sessions.tsx index 58e36ab..cf059b5 100644 --- a/apps/web/src/pages/sessions.tsx +++ b/apps/web/src/pages/sessions.tsx @@ -4,13 +4,13 @@ import { useNavigate } from "react-router-dom"; import { listProjects } from "../api/projects"; import { listRepositories } from "../api/git_repositories"; import { - getUserSessions, - deleteInstance, - stopInstance, - startInstance, - checkInstanceHealth, - recreateInstanceTunnel, - createInstance, + getUserSessions, + deleteInstance, + stopInstance, + startInstance, + checkInstanceHealth, + recreateInstanceTunnel, + createInstance, } from "../api/sessions"; import { listToolTypes } from "../api/tool_types"; import { getUserConfig, updateUserConfig } from "../api/settings"; @@ -24,582 +24,647 @@ type SessionsStatus = "loading" | "ready" | "error"; type CreateStatus = "idle" | "creating" | "error"; export const SessionsPage = () => { - const navigate = useNavigate(); - const [status, setStatus] = useState("loading"); - const [sessions, setSessions] = useState([]); - const [lastSessionId, setLastSessionId] = useState(null); + const navigate = useNavigate(); + const [status, setStatus] = useState("loading"); + const [sessions, setSessions] = useState([]); + const [lastSessionId, setLastSessionId] = useState(null); - const [projects, setProjects] = useState([]); - const [repositories, setRepositories] = useState([]); - const [toolTypes, setToolTypes] = useState([]); + const [projects, setProjects] = useState([]); + const [repositories, setRepositories] = useState([]); + const [toolTypes, setToolTypes] = useState([]); - const [selectedProject, setSelectedProject] = useState(""); - const [selectedRepo, setSelectedRepo] = useState(""); - const [selectedToolType, setSelectedToolType] = useState(""); - const [displayName, setDisplayName] = useState(""); - const [createStatus, setCreateStatus] = useState("idle"); - const [createError, setCreateError] = useState(null); + const [selectedProject, setSelectedProject] = useState(""); + const [selectedRepo, setSelectedRepo] = useState(""); + const [selectedToolType, setSelectedToolType] = useState(""); + const [displayName, setDisplayName] = useState(""); + const [createStatus, setCreateStatus] = useState("idle"); + const [createError, setCreateError] = useState(null); - const [deleteConfirmId, setDeleteConfirmId] = useState(null); - const [stopConfirmId, setStopConfirmId] = useState(null); - const [tunnelHealth, setTunnelHealth] = useState>({}); - const [recreatingId, setRecreatingId] = useState(null); + const [deleteConfirmId, setDeleteConfirmId] = useState(null); + const [stopConfirmId, setStopConfirmId] = useState(null); + const [tunnelHealth, setTunnelHealth] = useState< + Record< + string, + { healthy: boolean; status_code: number | null; error?: string } + > + >({}); + const [recreatingId, setRecreatingId] = useState(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"); - } - }, []); + 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(() => { + void loadSessions(); + }, [loadSessions]); - useEffect(() => { - const loadProjects = async () => { - try { - const data = await listProjects(); - setProjects(data); - } catch { - // ignore - } - }; - void loadProjects(); - }, []); + 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(() => { + const loadToolTypes = async () => { + try { + const data = await listToolTypes(); + setToolTypes(data); + } catch { + // ignore + } + }; + void loadToolTypes(); + }, []); - // Poll tunnel health every 30 seconds for running instances - useEffect(() => { - const checkHealth = async () => { - const runningSessions = sessions.filter( - (s) => s.status === "running" && s.url - ); - for (const session of runningSessions) { - try { - const health = await checkInstanceHealth( - session.project_id, - session.repository_id, - session.id - ); - setTunnelHealth((prev) => ({ - ...prev, - [session.id]: health, - })); - } catch { - setTunnelHealth((prev) => ({ - ...prev, - [session.id]: { healthy: false, status_code: null, error: "check failed" }, - })); - } - } - }; + // Poll tunnel health every 30 seconds for running instances + useEffect(() => { + const checkHealth = async () => { + const runningSessions = sessions.filter( + (s) => s.status === "running" && s.url, + ); + for (const session of runningSessions) { + try { + const health = await checkInstanceHealth( + session.project_id, + session.repository_id, + session.id, + ); + setTunnelHealth((prev) => ({ + ...prev, + [session.id]: health, + })); + } catch { + setTunnelHealth((prev) => ({ + ...prev, + [session.id]: { + healthy: false, + status_code: null, + error: "check failed", + }, + })); + } + } + }; - // Check immediately and then every 30 seconds - void checkHealth(); - const interval = setInterval(() => void checkHealth(), 30000); - return () => clearInterval(interval); - }, [sessions]); + // Check immediately and then every 30 seconds + void checkHealth(); + const interval = setInterval(() => void checkHealth(), 30000); + return () => clearInterval(interval); + }, [sessions]); - useEffect(() => { - if (!selectedProject) { - setRepositories([]); - return; - } - const loadRepos = async () => { - try { - const data = await listRepositories(selectedProject); - setRepositories(data); - } catch { - setRepositories([]); - } - }; - void loadRepos(); - }, [selectedProject]); + 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) => ["running", "building", "pending"].includes(s.status)), - [sessions] - ); + 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 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 lastSession = useMemo( + () => sessions.find((s) => s.id === lastSessionId) ?? null, + [sessions, lastSessionId], + ); - const handleCreate = async (e: React.FormEvent) => { - e.preventDefault(); - setCreateError(null); + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + setCreateError(null); - if (!selectedProject || !selectedRepo || !selectedToolType) { - setCreateError("Project, repository, and tool type are required"); - return; - } + 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 - ); - - // Auto-start the instance - await startInstance(selectedProject, selectedRepo, instance.id); - - await updateUserConfig({ last_session_id: instance.id }); - setCreateStatus("idle"); - setSelectedProject(""); - setSelectedRepo(""); - setSelectedToolType(""); - setDisplayName(""); - await loadSessions(); - } catch { - setCreateStatus("error"); - setCreateError("Failed to create session"); - } - }; + setCreateStatus("creating"); + try { + const instance = await createInstance( + selectedProject, + selectedRepo, + selectedToolType, + displayName || undefined, + ); - const handleStop = async (sessionId: string, projectId: string, repoId: string) => { - try { - await stopInstance(projectId, repoId, sessionId); - setStopConfirmId(null); - await loadSessions(); - } catch { - setStopConfirmId(null); - } - }; + // Auto-start the instance + await startInstance(selectedProject, selectedRepo, instance.id); - const handleDelete = async (sessionId: string, projectId: string, repoId: string) => { - try { - await deleteInstance(projectId, repoId, sessionId); - setDeleteConfirmId(null); - // Remove from local state immediately - setSessions((prev) => prev.filter((s) => s.id !== sessionId)); - } catch { - setDeleteConfirmId(null); - } - }; + await updateUserConfig({ last_session_id: instance.id }); + setCreateStatus("idle"); + setSelectedProject(""); + setSelectedRepo(""); + setSelectedToolType(""); + setDisplayName(""); + await loadSessions(); + } catch { + setCreateStatus("error"); + setCreateError("Failed to create session"); + } + }; - const handleRecreateTunnel = async (session: Session) => { - setRecreatingId(session.id); - try { - await recreateInstanceTunnel( - session.project_id, - session.repository_id, - session.id - ); - // Refresh sessions to get new URL - await loadSessions(); - } catch { - // ignore - } finally { - setRecreatingId(null); - } - }; + const handleStop = async ( + sessionId: string, + projectId: string, + repoId: string, + ) => { + try { + await stopInstance(projectId, repoId, sessionId); + setStopConfirmId(null); + await loadSessions(); + } catch { + setStopConfirmId(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}`); - } - }; + const handleDelete = async ( + sessionId: string, + projectId: string, + repoId: string, + ) => { + try { + await deleteInstance(projectId, repoId, sessionId); + setDeleteConfirmId(null); + // Remove from local state immediately + setSessions((prev) => prev.filter((s) => s.id !== sessionId)); + } catch { + setDeleteConfirmId(null); + } + }; - 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}`); - } - }; + const handleRecreateTunnel = async (session: Session) => { + setRecreatingId(session.id); + try { + await recreateInstanceTunnel( + session.project_id, + session.repository_id, + session.id, + ); + // Refresh sessions to get new URL + await loadSessions(); + } catch { + // ignore + } finally { + setRecreatingId(null); + } + }; - return ( -
-
-

Sessions

-
+ 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}`); + } + }; - {status === "loading" &&

Loading sessions...

} + 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}`); + } + }; - {status === "error" && ( -
-

Failed to load sessions

- -
- )} + return ( +
+
+

Sessions

+
- {status === "ready" && ( - <> - {/* Last Session */} - {lastSession && ( -
-

Last Session

-
-
-

{lastSession.display_name || lastSession.tool_type_name || "Unnamed Session"}

-

- {lastSession.tool_type_name} · {lastSession.project_name} · {lastSession.repository_name} -

- {lastSession.url && ( -

- - {lastSession.url} - -

- )} - {lastSession.status} -
-
- {lastSession.url ? ( - - - Open - - ) : ( - - )} -
-
-
- )} + {status === "loading" &&

Loading sessions...

} - {/* Active Sessions */} -
-

- Active Sessions - {activeSessions.length > 0 && ( - {activeSessions.length} - )} -

- {activeSessions.length === 0 ? ( -

No active sessions

- ) : ( -
- {activeSessions.map((session) => ( -
-
-

{session.display_name || session.tool_type_name || "Unnamed Session"}

-

- {session.tool_type_name} · {session.project_name} -

- {session.url && ( -

- - {session.url} - -

- )} - {session.status} - {tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && ( - tunnel error - )} -
-
- {session.url ? ( - - - Open - - ) : ( - - )} - {tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && ( - - )} - {stopConfirmId === session.id ? ( -
- Stop? - - -
- ) : ( - - )} - {deleteConfirmId === session.id ? ( -
- - -
- ) : ( - - )} -
-
- ))} -
- )} -
+ {status === "error" && ( +
+

Failed to load sessions

+ +
+ )} - {/* Recent Sessions */} - {recentSessions.length > 0 && ( -
-

Recent Sessions

-
- {recentSessions.map((session) => ( -
-
- {session.display_name || session.tool_type_name || "Unnamed Session"} - - {session.tool_type_name} · {session.project_name} - -
-
- {session.url ? ( - - Open - - ) : ( - - )} - {deleteConfirmId === session.id ? ( -
- - -
- ) : ( - - )} -
-
- ))} -
-
- )} + {status === "ready" && ( + <> + {/* Last Session */} + {lastSession && ( +
+

Last Session

+
+
+

+ {lastSession.display_name || + lastSession.tool_type_name || + "Unnamed Session"} +

+

+ {lastSession.tool_type_name} · {lastSession.project_name} ·{" "} + {lastSession.repository_name} +

+ {lastSession.url && ( +

+ + {lastSession.url} + +

+ )} + + {lastSession.status} + +
+
+ {lastSession.url ? ( + + + Open + + ) : ( + + )} +
+
+
+ )} - {/* Create Session */} -
-

Create New Session

-
-
- + {/* Active Sessions */} +
+

+ Active Sessions + {activeSessions.length > 0 && ( + {activeSessions.length} + )} +

+ {activeSessions.length === 0 ? ( +

No active sessions

+ ) : ( +
+ {activeSessions.map((session) => ( +
+
+

+ {session.display_name || + session.tool_type_name || + "Unnamed Session"} +

+

+ {session.tool_type_name} · {session.project_name} +

+ {session.url && ( +

+ + {session.url} + +

+ )} + + {session.status} + + {tunnelHealth[session.id] && + !tunnelHealth[session.id].healthy && ( + + tunnel error + + )} +
+
+ {session.url ? ( + + + Open + + ) : ( + + )} + {tunnelHealth[session.id] && + !tunnelHealth[session.id].healthy && ( + + )} + {stopConfirmId === session.id ? ( +
+ Stop? + + +
+ ) : ( + + )} + {deleteConfirmId === session.id ? ( +
+ + +
+ ) : ( + + )} +
+
+ ))} +
+ )} +
- + {/* Recent Sessions */} + {recentSessions.length > 0 && ( +
+

Recent Sessions

+
+ {recentSessions.map((session) => ( +
+
+ + {session.display_name || + session.tool_type_name || + "Unnamed Session"} + + + {session.tool_type_name} · {session.project_name} + +
+
+ {session.url ? ( + + Open + + ) : ( + + )} + {deleteConfirmId === session.id ? ( +
+ + +
+ ) : ( + + )} +
+
+ ))} +
+
+ )} - -
+ {/* Create Session */} +
+

Create New Session

+ +
+ - + - {createError &&

{createError}

} + +
-
- -
- -
- - )} -
- ); + + + {createError &&

{createError}

} + +
+ +
+ + + + )} +
+ ); }; diff --git a/apps/web/src/pages/tool-configs.tsx b/apps/web/src/pages/tool-configs.tsx index 37b0ef2..26f5f37 100644 --- a/apps/web/src/pages/tool-configs.tsx +++ b/apps/web/src/pages/tool-configs.tsx @@ -6,350 +6,386 @@ import type { ToolType } from "../types/tool-type"; import type { ToolConfig } from "../types/tool-config"; import { listToolTypes } from "../api/tool_types"; import { - createToolConfig, - deleteToolConfig, - listToolConfigs, - updateToolConfig, + createToolConfig, + deleteToolConfig, + listToolConfigs, + updateToolConfig, } from "../api/tool_configs"; type ConfigStatus = "loading" | "ready" | "error"; export const ToolConfigsPage = () => { - const navigate = useNavigate(); - const [status, setStatus] = useState("loading"); - const [toolTypes, setToolTypes] = useState([]); - const [configs, setConfigs] = useState([]); - const [selectedToolType, setSelectedToolType] = useState(""); - const [showForm, setShowForm] = useState(false); - const [editingConfig, setEditingConfig] = useState(null); - const [formData, setFormData] = useState({ - key: "", - value: "", - config_type: "env", - file_path: "", - }); - const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle"); + const navigate = useNavigate(); + const [status, setStatus] = useState("loading"); + const [toolTypes, setToolTypes] = useState([]); + const [configs, setConfigs] = useState([]); + const [selectedToolType, setSelectedToolType] = useState(""); + const [showForm, setShowForm] = useState(false); + const [editingConfig, setEditingConfig] = useState(null); + const [formData, setFormData] = useState({ + key: "", + value: "", + config_type: "env", + file_path: "", + }); + const [saveStatus, setSaveStatus] = useState< + "idle" | "saving" | "saved" | "error" + >("idle"); - const loadData = useCallback(async () => { - try { - const [typesData, configsData] = await Promise.all([ - listToolTypes(), - listToolConfigs(), - ]); - setToolTypes(typesData); - setConfigs(configsData); - if (typesData.length > 0 && !selectedToolType) { - setSelectedToolType(typesData[0].id); - } - setStatus("ready"); - } catch { - setStatus("error"); - } - }, [selectedToolType]); + const loadData = useCallback(async () => { + try { + const [typesData, configsData] = await Promise.all([ + listToolTypes(), + listToolConfigs(), + ]); + setToolTypes(typesData); + setConfigs(configsData); + if (typesData.length > 0 && !selectedToolType) { + setSelectedToolType(typesData[0].id); + } + setStatus("ready"); + } catch { + setStatus("error"); + } + }, [selectedToolType]); - useEffect(() => { - void loadData(); - }, [loadData]); + useEffect(() => { + void loadData(); + }, [loadData]); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setSaveStatus("saving"); - try { - const data = { - tool_type_id: selectedToolType, - key: formData.key, - value: formData.value, - config_type: formData.config_type, - file_path: formData.config_type === "file" ? formData.file_path : undefined, - }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaveStatus("saving"); + try { + const data = { + tool_type_id: selectedToolType, + key: formData.key, + value: formData.value, + config_type: formData.config_type, + file_path: + formData.config_type === "file" ? formData.file_path : undefined, + }; - if (editingConfig) { - await updateToolConfig(editingConfig.id, data); - } else { - await createToolConfig(data); - } + if (editingConfig) { + await updateToolConfig(editingConfig.id, data); + } else { + await createToolConfig(data); + } - setSaveStatus("saved"); - setShowForm(false); - setEditingConfig(null); - setFormData({ key: "", value: "", config_type: "env", file_path: "" }); - await loadData(); - } catch { - setSaveStatus("error"); - } - }; + setSaveStatus("saved"); + setShowForm(false); + setEditingConfig(null); + setFormData({ key: "", value: "", config_type: "env", file_path: "" }); + await loadData(); + } catch { + setSaveStatus("error"); + } + }; - const handleEdit = (config: ToolConfig) => { - setEditingConfig(config); - setFormData({ - key: config.key, - value: config.value, - config_type: config.config_type, - file_path: config.file_path || "", - }); - setSelectedToolType(config.tool_type_id); - setShowForm(true); - }; + const handleEdit = (config: ToolConfig) => { + setEditingConfig(config); + setFormData({ + key: config.key, + value: config.value, + config_type: config.config_type, + file_path: config.file_path || "", + }); + setSelectedToolType(config.tool_type_id); + setShowForm(true); + }; - const handleDelete = async (id: string) => { - if (!window.confirm("Delete this config?")) return; - try { - await deleteToolConfig(id); - await loadData(); - } catch { - // Error handled by UI state - } - }; + const handleDelete = async (id: string) => { + if (!window.confirm("Delete this config?")) return; + try { + await deleteToolConfig(id); + await loadData(); + } catch { + // Error handled by UI state + } + }; - const filteredConfigs = configs.filter( - (c) => c.tool_type_id === selectedToolType - ); + const filteredConfigs = configs.filter( + (c) => c.tool_type_id === selectedToolType, + ); - const selectedTool = toolTypes.find((t) => t.id === selectedToolType); + const selectedTool = toolTypes.find((t) => t.id === selectedToolType); - if (status === "loading") { - return ( -
-
-

Tool Configurations

-
-

Loading...

-
- ); - } + if (status === "loading") { + return ( +
+
+

Tool Configurations

+
+

Loading...

+
+ ); + } - if (status === "error") { - return ( -
-
-

Tool Configurations

-
-
-

Failed to load configurations

- -
-
- ); - } + if (status === "error") { + return ( +
+
+

Tool Configurations

+
+
+

Failed to load configurations

+ +
+
+ ); + } - return ( -
-
-
-

Settings

-

Tool Configurations

-
- -

- Manage environment variables and configuration files for your tools -

-
+ return ( +
+
+
+

Settings

+

Tool Configurations

+
+ +

+ Manage environment variables and configuration files for your tools +

+
- {/* Tool Type Selector */} -
- - - {selectedTool && ( -

- Category: {selectedTool.category} · Interfaces: {selectedTool.interfaces?.join(", ")} -

- )} -
+ {/* Tool Type Selector */} +
+ + + {selectedTool && ( +

+ Category: {selectedTool.category} · Interfaces:{" "} + {selectedTool.interfaces?.join(", ")} +

+ )} +
- {/* Config List */} -
-
-

Configuration Variables

- -
+ {/* Config List */} +
+
+

Configuration Variables

+ +
- {filteredConfigs.length === 0 ? ( -

No configurations for this tool yet.

- ) : ( -
- {filteredConfigs.map((config) => ( -
-
-
- {config.key} - - {config.config_type} - -
-

- {config.config_type === "file" && config.file_path - ? `File: ${config.file_path}` - : "Environment variable"} -

-
-
- - -
-
- ))} -
- )} -
+ {filteredConfigs.length === 0 ? ( +

No configurations for this tool yet.

+ ) : ( +
+ {filteredConfigs.map((config) => ( +
+
+
+ {config.key} + + {config.config_type} + +
+

+ {config.config_type === "file" && config.file_path + ? `File: ${config.file_path}` + : "Environment variable"} +

+
+
+ + +
+
+ ))} +
+ )} +
- {/* Add/Edit Form */} - {showForm && ( -
-

{editingConfig ? "Edit Config" : "Add Config"}

-
-
- - setFormData({ ...formData, key: e.target.value })} - placeholder="e.g., OPENAI_API_KEY" - className="form-input" - required - /> -
+ {/* Add/Edit Form */} + {showForm && ( +
+

{editingConfig ? "Edit Config" : "Add Config"}

+ +
+ + + setFormData({ ...formData, key: e.target.value }) + } + placeholder="e.g., OPENAI_API_KEY" + className="form-input" + required + /> +
-
- - -
+
+ + +
- {formData.config_type === "file" && ( -
- - - setFormData({ ...formData, file_path: e.target.value }) - } - placeholder="e.g., /app/config.json" - className="form-input" - required - /> -
- )} + {formData.config_type === "file" && ( +
+ + + setFormData({ ...formData, file_path: e.target.value }) + } + placeholder="e.g., /app/config.json" + className="form-input" + required + /> +
+ )} -
- -