refactor: centralize types and extract seed data (Task 1.1)

- Create types/ directory with canonical domain type definitions
  - session.ts, tool-instance.ts, tool-type.ts, git-repository.ts
  - config-folder.ts, tool-config.ts, project.ts, user.ts, api-response.ts
- Move inline types from api modules to types/ with backward-compatible re-exports
- Update all consumers (pages, components, state) to import from types/
- Extract seed_builtin_tool_types from main.py to seeds/builtin_tool_types.py
- Ensure Session, ToolInstance, ToolType, GitRepository defined exactly once

Quality gates: tsc (pass), eslint (pass), Python syntax (pass)
Refs: repo-restructure Task 1.1
This commit is contained in:
Developer
2026-06-02 18:58:50 +00:00
parent ee1fa6bee5
commit 985ca538e3
26 changed files with 3622 additions and 3210 deletions
+8 -8
View File
@@ -24,7 +24,7 @@ export const getConfigFolder = async (id: string): Promise<ConfigFolder> => {
}; };
export const createConfigFolder = async ( export const createConfigFolder = async (
data: CreateConfigFolderRequest data: CreateConfigFolderRequest,
): Promise<ConfigFolder> => { ): Promise<ConfigFolder> => {
const response = await apiClient.post<ConfigFolder>("/config-folders", data); const response = await apiClient.post<ConfigFolder>("/config-folders", data);
return response.data; return response.data;
@@ -32,11 +32,11 @@ export const createConfigFolder = async (
export const updateConfigFolder = async ( export const updateConfigFolder = async (
id: string, id: string,
data: UpdateConfigFolderRequest data: UpdateConfigFolderRequest,
): Promise<ConfigFolder> => { ): Promise<ConfigFolder> => {
const response = await apiClient.put<ConfigFolder>( const response = await apiClient.put<ConfigFolder>(
`/config-folders/${id}`, `/config-folders/${id}`,
data data,
); );
return response.data; return response.data;
}; };
@@ -48,11 +48,11 @@ export const deleteConfigFolder = async (id: string): Promise<void> => {
export const addProjectOverride = async ( export const addProjectOverride = async (
id: string, id: string,
projectId: string, projectId: string,
data: ProjectOverrideRequest data: ProjectOverrideRequest,
): Promise<ConfigFolder> => { ): Promise<ConfigFolder> => {
const response = await apiClient.post<ConfigFolder>( const response = await apiClient.post<ConfigFolder>(
`/config-folders/${id}/overrides/${projectId}`, `/config-folders/${id}/overrides/${projectId}`,
data data,
); );
return response.data; return response.data;
}; };
@@ -60,18 +60,18 @@ export const addProjectOverride = async (
export const updateProjectOverride = async ( export const updateProjectOverride = async (
id: string, id: string,
projectId: string, projectId: string,
data: ProjectOverrideRequest data: ProjectOverrideRequest,
): Promise<ConfigFolder> => { ): Promise<ConfigFolder> => {
const response = await apiClient.put<ConfigFolder>( const response = await apiClient.put<ConfigFolder>(
`/config-folders/${id}/overrides/${projectId}`, `/config-folders/${id}/overrides/${projectId}`,
data data,
); );
return response.data; return response.data;
}; };
export const deleteProjectOverride = async ( export const deleteProjectOverride = async (
id: string, id: string,
projectId: string projectId: string,
): Promise<void> => { ): Promise<void> => {
await apiClient.delete(`/config-folders/${id}/overrides/${projectId}`); await apiClient.delete(`/config-folders/${id}/overrides/${projectId}`);
}; };
+39 -27
View File
@@ -23,24 +23,34 @@ export type {
} from "../types/git-repository"; } from "../types/git-repository";
export async function parseGitUrl(url: string): Promise<URLParseResult> { export async function parseGitUrl(url: string): Promise<URLParseResult> {
const response = await apiClient.post("/projects/repositories/parse-url", { url }); const response = await apiClient.post("/projects/repositories/parse-url", {
url,
});
return response.data; return response.data;
} }
export async function listRepositories(projectId: string): Promise<GitRepository[]> { export async function listRepositories(
projectId: string,
): Promise<GitRepository[]> {
const response = await apiClient.get(`/projects/${projectId}/repositories`); const response = await apiClient.get(`/projects/${projectId}/repositories`);
return response.data; return response.data;
} }
export async function createRepository( export async function createRepository(
projectId: string, projectId: string,
data: GitRepositoryCreate data: GitRepositoryCreate,
): Promise<GitRepository> { ): Promise<GitRepository> {
const response = await apiClient.post(`/projects/${projectId}/repositories`, data); const response = await apiClient.post(
`/projects/${projectId}/repositories`,
data,
);
return response.data; return response.data;
} }
export async function deleteRepository(projectId: string, repoId: string): Promise<void> { export async function deleteRepository(
projectId: string,
repoId: string,
): Promise<void> {
await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`); await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`);
} }
@@ -48,34 +58,36 @@ export async function getRepositoryHistory(
projectId: string, projectId: string,
repoId: string, repoId: string,
branch?: string, branch?: string,
limit?: number limit?: number,
): Promise<CommitHistoryResponse> { ): Promise<CommitHistoryResponse> {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (branch) searchParams.set("branch", branch); if (branch) searchParams.set("branch", branch);
if (limit) searchParams.set("limit", String(limit)); if (limit) searchParams.set("limit", String(limit));
const queryString = searchParams.toString(); const queryString = searchParams.toString();
const params = queryString ? `?${queryString}` : ""; const params = queryString ? `?${queryString}` : "";
const response = await apiClient.get(`/projects/${projectId}/repositories/${repoId}/history${params}`); const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/history${params}`,
);
return response.data; return response.data;
} }
export async function getCommitDetail( export async function getCommitDetail(
projectId: string, projectId: string,
repoId: string, repoId: string,
commitHash: string commitHash: string,
): Promise<CommitDetail> { ): Promise<CommitDetail> {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/commits/${commitHash}` `/projects/${projectId}/repositories/${repoId}/commits/${commitHash}`,
); );
return response.data; return response.data;
} }
export async function getRepositoryStatus( export async function getRepositoryStatus(
projectId: string, projectId: string,
repoId: string repoId: string,
): Promise<GitStatus> { ): Promise<GitStatus> {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/status` `/projects/${projectId}/repositories/${repoId}/status`,
); );
return response.data; return response.data;
} }
@@ -84,11 +96,11 @@ export async function createBranch(
projectId: string, projectId: string,
repoId: string, repoId: string,
name: string, name: string,
baseBranch: string = "HEAD" baseBranch: string = "HEAD",
): Promise<{ message: string; branch: string }> { ): Promise<{ message: string; branch: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/branches`, `/projects/${projectId}/repositories/${repoId}/branches`,
{ name, base_branch: baseBranch } { name, base_branch: baseBranch },
); );
return response.data; return response.data;
} }
@@ -97,10 +109,10 @@ export async function deleteBranch(
projectId: string, projectId: string,
repoId: string, repoId: string,
branchName: string, branchName: string,
force: boolean = false force: boolean = false,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const response = await apiClient.delete( const response = await apiClient.delete(
`/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}` `/projects/${projectId}/repositories/${repoId}/branches/${branchName}?force=${force}`,
); );
return response.data; return response.data;
} }
@@ -108,11 +120,11 @@ export async function deleteBranch(
export async function checkoutBranch( export async function checkoutBranch(
projectId: string, projectId: string,
repoId: string, repoId: string,
branch: string branch: string,
): Promise<{ message: string; branch: string }> { ): Promise<{ message: string; branch: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/checkout`, `/projects/${projectId}/repositories/${repoId}/checkout`,
{ branch } { branch },
); );
return response.data; return response.data;
} }
@@ -121,21 +133,21 @@ export async function commitChanges(
projectId: string, projectId: string,
repoId: string, repoId: string,
message: string, message: string,
files?: string[] files?: string[],
): Promise<CommitResponse> { ): Promise<CommitResponse> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/commit`, `/projects/${projectId}/repositories/${repoId}/commit`,
{ message, files } { message, files },
); );
return response.data; return response.data;
} }
export async function fetchRepository( export async function fetchRepository(
projectId: string, projectId: string,
repoId: string repoId: string,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/fetch` `/projects/${projectId}/repositories/${repoId}/fetch`,
); );
return response.data; return response.data;
} }
@@ -143,11 +155,11 @@ export async function fetchRepository(
export async function pullRepository( export async function pullRepository(
projectId: string, projectId: string,
repoId: string, repoId: string,
branch?: string branch?: string,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const params = branch ? `?branch=${branch}` : ""; const params = branch ? `?branch=${branch}` : "";
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/pull${params}` `/projects/${projectId}/repositories/${repoId}/pull${params}`,
); );
return response.data; return response.data;
} }
@@ -155,11 +167,11 @@ export async function pullRepository(
export async function pushRepository( export async function pushRepository(
projectId: string, projectId: string,
repoId: string, repoId: string,
branch?: string branch?: string,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const params = branch ? `?branch=${branch}` : ""; const params = branch ? `?branch=${branch}` : "";
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/push${params}` `/projects/${projectId}/repositories/${repoId}/push${params}`,
); );
return response.data; return response.data;
} }
@@ -169,11 +181,11 @@ export async function mergeBranches(
repoId: string, repoId: string,
sourceBranch: string, sourceBranch: string,
targetBranch?: string, targetBranch?: string,
message?: string message?: string,
): Promise<MergeResponse> { ): Promise<MergeResponse> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/merge`, `/projects/${projectId}/repositories/${repoId}/merge`,
{ source_branch: sourceBranch, target_branch: targetBranch, message } { source_branch: sourceBranch, target_branch: targetBranch, message },
); );
return response.data; return response.data;
} }
+16 -16
View File
@@ -7,10 +7,10 @@ export type { ToolInstance } from "../types/tool-instance";
export async function listInstances( export async function listInstances(
projectId: string, projectId: string,
repoId: string repoId: string,
): Promise<ToolInstance[]> { ): Promise<ToolInstance[]> {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/instances` `/projects/${projectId}/repositories/${repoId}/instances`,
); );
return response.data.instances; return response.data.instances;
} }
@@ -19,14 +19,14 @@ export async function createInstance(
projectId: string, projectId: string,
repoId: string, repoId: string,
toolTypeId: string, toolTypeId: string,
displayName?: string displayName?: string,
): Promise<ToolInstance> { ): Promise<ToolInstance> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances`, `/projects/${projectId}/repositories/${repoId}/instances`,
{ {
tool_type_id: toolTypeId, tool_type_id: toolTypeId,
display_name: displayName, display_name: displayName,
} },
); );
return response.data; return response.data;
} }
@@ -34,10 +34,10 @@ export async function createInstance(
export async function startInstance( export async function startInstance(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<{ status: string; url?: string }> { ): Promise<{ status: string; url?: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start` `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`,
); );
return response.data; return response.data;
} }
@@ -45,10 +45,10 @@ export async function startInstance(
export async function stopInstance( export async function stopInstance(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<{ status: string }> { ): Promise<{ status: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/stop` `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/stop`,
); );
return response.data; return response.data;
} }
@@ -56,10 +56,10 @@ export async function stopInstance(
export async function restartInstance( export async function restartInstance(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<{ status: string; url?: string }> { ): Promise<{ status: string; url?: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart` `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`,
); );
return response.data; return response.data;
} }
@@ -67,10 +67,10 @@ export async function restartInstance(
export async function deleteInstance( export async function deleteInstance(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<void> { ): Promise<void> {
await apiClient.delete( await apiClient.delete(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}` `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`,
); );
} }
@@ -82,10 +82,10 @@ export async function getUserSessions(): Promise<Session[]> {
export async function checkInstanceHealth( export async function checkInstanceHealth(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<{ healthy: boolean; status_code: number | null; error?: string }> { ): Promise<{ healthy: boolean; status_code: number | null; error?: string }> {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health` `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/health`,
); );
return response.data; return response.data;
} }
@@ -93,10 +93,10 @@ export async function checkInstanceHealth(
export async function recreateInstanceTunnel( export async function recreateInstanceTunnel(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<{ status: string; url?: string }> { ): Promise<{ status: string; url?: string }> {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/recreate-tunnel` `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/recreate-tunnel`,
); );
return response.data; return response.data;
} }
+11 -8
View File
@@ -5,32 +5,35 @@ export type { ToolConfig, CreateToolConfigRequest } from "../types/tool-config";
export const listToolConfigs = async ( export const listToolConfigs = async (
tool_type_id?: string, tool_type_id?: string,
project_id?: string project_id?: string,
): Promise<ToolConfig[]> => { ): Promise<ToolConfig[]> => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (tool_type_id) params.append("tool_type_id", tool_type_id); if (tool_type_id) params.append("tool_type_id", tool_type_id);
if (project_id) params.append("project_id", project_id); if (project_id) params.append("project_id", project_id);
const response = await apiClient.get<{ configs: ToolConfig[] }>( const response = await apiClient.get<{ configs: ToolConfig[] }>(
`/tool-configs?${params.toString()}` `/tool-configs?${params.toString()}`,
); );
return response.data.configs; return response.data.configs;
}; };
export const createToolConfig = async ( export const createToolConfig = async (
data: CreateToolConfigRequest data: CreateToolConfigRequest,
): Promise<ToolConfig> => { ): Promise<ToolConfig> => {
const response = await apiClient.post<{ configs: ToolConfig[] }>("/tool-configs", data); const response = await apiClient.post<{ configs: ToolConfig[] }>(
"/tool-configs",
data,
);
return response.data.configs[0]; return response.data.configs[0];
}; };
export const updateToolConfig = async ( export const updateToolConfig = async (
id: string, id: string,
data: CreateToolConfigRequest data: CreateToolConfigRequest,
): Promise<ToolConfig> => { ): Promise<ToolConfig> => {
const response = await apiClient.put<{ configs: ToolConfig[] }>( const response = await apiClient.put<{ configs: ToolConfig[] }>(
`/tool-configs/${id}`, `/tool-configs/${id}`,
data data,
); );
return response.data.configs[0]; return response.data.configs[0];
}; };
@@ -40,10 +43,10 @@ export const deleteToolConfig = async (id: string): Promise<void> => {
}; };
export const getToolConfigDefaults = async ( export const getToolConfigDefaults = async (
toolTypeId: string toolTypeId: string,
): Promise<ToolConfig> => { ): Promise<ToolConfig> => {
const response = await apiClient.get<ToolConfig>( const response = await apiClient.get<ToolConfig>(
`/tool-configs/defaults/${toolTypeId}` `/tool-configs/defaults/${toolTypeId}`,
); );
return response.data; return response.data;
}; };
+24 -6
View File
@@ -1,7 +1,16 @@
import { apiClient } from "./client"; 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<ToolType[]> => { export const listToolTypes = async (): Promise<ToolType[]> => {
const response = await apiClient.get<ToolType[]>("/tool-types"); const response = await apiClient.get<ToolType[]>("/tool-types");
@@ -13,12 +22,17 @@ export const getToolType = async (id: string): Promise<ToolType> => {
return response.data; return response.data;
}; };
export const createToolType = async (data: CreateToolTypeRequest): Promise<ToolType> => { export const createToolType = async (
data: CreateToolTypeRequest,
): Promise<ToolType> => {
const response = await apiClient.post<ToolType>("/tool-types", data); const response = await apiClient.post<ToolType>("/tool-types", data);
return response.data; return response.data;
}; };
export const updateToolType = async (id: string, data: UpdateToolTypeRequest): Promise<ToolType> => { export const updateToolType = async (
id: string,
data: UpdateToolTypeRequest,
): Promise<ToolType> => {
const response = await apiClient.put<ToolType>(`/tool-types/${id}`, data); const response = await apiClient.put<ToolType>(`/tool-types/${id}`, data);
return response.data; return response.data;
}; };
@@ -27,7 +41,11 @@ export const deleteToolType = async (id: string): Promise<void> => {
await apiClient.delete(`/tool-types/${id}`); await apiClient.delete(`/tool-types/${id}`);
}; };
export const validateToolType = async (id: string): Promise<{ valid: boolean; errors?: string[] }> => { export const validateToolType = async (
const response = await apiClient.get<{ valid: boolean; errors?: string[] }>(`/tool-types/${id}/validate`); id: string,
): Promise<{ valid: boolean; errors?: string[] }> => {
const response = await apiClient.get<{ valid: boolean; errors?: string[] }>(
`/tool-types/${id}/validate`,
);
return response.data; return response.data;
}; };
+11 -5
View File
@@ -13,14 +13,15 @@ const NAV_ITEMS: { to: string; label: string; icon: IconName }[] = [
{ to: "/", label: "Home", icon: "dashboard" }, { to: "/", label: "Home", icon: "dashboard" },
{ to: "/projects", label: "Projects", icon: "projects" }, { to: "/projects", label: "Projects", icon: "projects" },
{ to: "/tool-workshop", label: "Tool Workshop", icon: "settings" }, { to: "/tool-workshop", label: "Tool Workshop", icon: "settings" },
{ to: "/settings", label: "Settings", icon: "settings" } { to: "/settings", label: "Settings", icon: "settings" },
]; ];
const ACTIVE_STATUSES = ["running", "building", "pending"]; const ACTIVE_STATUSES = ["running", "building", "pending"];
const SessionItem = ({ session }: { session: Session }) => { const SessionItem = ({ session }: { session: Session }) => {
const isRunning = session.status === "running"; const isRunning = session.status === "running";
const displayName = session.display_name || session.tool_type_name || "Unnamed Session"; const displayName =
session.display_name || session.tool_type_name || "Unnamed Session";
return ( return (
<a <a
@@ -87,12 +88,16 @@ export const AppShell = () => {
<aside className="shell-nav" aria-label="Primary navigation"> <aside className="shell-nav" aria-label="Primary navigation">
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {
const isHome = item.to === "/"; const isHome = item.to === "/";
const activeCount = sessions.filter((s) => s.status === "running").length; const activeCount = sessions.filter(
(s) => s.status === "running",
).length;
return ( return (
<NavLink <NavLink
key={item.to} key={item.to}
to={item.to} to={item.to}
className={({ isActive }) => (isActive ? "nav-item nav-item-active" : "nav-item")} className={({ isActive }) =>
isActive ? "nav-item nav-item-active" : "nav-item"
}
end={item.to === "/"} end={item.to === "/"}
> >
<Icon name={item.icon} size="sm" /> <Icon name={item.icon} size="sm" />
@@ -104,7 +109,8 @@ export const AppShell = () => {
); );
})} })}
{sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length > 0 && ( {sessions.filter((s) => ACTIVE_STATUSES.includes(s.status)).length >
0 && (
<> <>
<div className="nav-divider" /> <div className="nav-divider" />
<div className="nav-section-title">Live sessions</div> <div className="nav-section-title">Live sessions</div>
+48 -19
View File
@@ -14,7 +14,8 @@ import {
stopInstance, stopInstance,
} from "../api/sessions"; } 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 { interface InstanceListProps {
projectId: string; projectId: string;
@@ -22,7 +23,11 @@ interface InstanceListProps {
toolTypes: ToolType[]; toolTypes: ToolType[];
} }
export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps) => { export const InstanceList = ({
projectId,
repoId,
toolTypes,
}: InstanceListProps) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [instances, setInstances] = useState<ToolInstance[]>([]); const [instances, setInstances] = useState<ToolInstance[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -35,7 +40,9 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null); const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
// Health check state // Health check state
const [healthStatus, setHealthStatus] = useState<Record<string, { healthy: boolean; lastCheck: number }>>({}); const [healthStatus, setHealthStatus] = useState<
Record<string, { healthy: boolean; lastCheck: number }>
>({});
const loadInstances = useCallback(async () => { const loadInstances = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -55,21 +62,27 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
// Health check polling // Health check polling
useEffect(() => { useEffect(() => {
const runningInstances = instances.filter(i => i.status === "running" && i.url?.startsWith("http")); const runningInstances = instances.filter(
(i) => i.status === "running" && i.url?.startsWith("http"),
);
if (runningInstances.length === 0) return; if (runningInstances.length === 0) return;
const checkHealth = async () => { const checkHealth = async () => {
for (const instance of runningInstances) { for (const instance of runningInstances) {
try { try {
const health = await checkInstanceHealth(projectId, repoId, instance.id); const health = await checkInstanceHealth(
setHealthStatus(prev => ({ projectId,
repoId,
instance.id,
);
setHealthStatus((prev) => ({
...prev, ...prev,
[instance.id]: { healthy: health.healthy, lastCheck: Date.now() } [instance.id]: { healthy: health.healthy, lastCheck: Date.now() },
})); }));
} catch { } catch {
setHealthStatus(prev => ({ setHealthStatus((prev) => ({
...prev, ...prev,
[instance.id]: { healthy: false, lastCheck: Date.now() } [instance.id]: { healthy: false, lastCheck: Date.now() },
})); }));
} }
} }
@@ -87,7 +100,12 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
if (!selectedToolType) return; if (!selectedToolType) return;
setError(null); setError(null);
try { try {
await createInstance(projectId, repoId, selectedToolType, displayName || undefined); await createInstance(
projectId,
repoId,
selectedToolType,
displayName || undefined,
);
setShowCreate(false); setShowCreate(false);
setSelectedToolType(""); setSelectedToolType("");
setDisplayName(""); setDisplayName("");
@@ -130,7 +148,7 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
try { try {
await deleteInstance(projectId, repoId, instanceId); await deleteInstance(projectId, repoId, instanceId);
// Update state immediately instead of reloading // Update state immediately instead of reloading
setInstances(prev => prev.filter(i => i.id !== instanceId)); setInstances((prev) => prev.filter((i) => i.id !== instanceId));
} catch { } catch {
setError("Failed to delete instance"); setError("Failed to delete instance");
} }
@@ -181,9 +199,7 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
</button> </button>
</div> </div>
{error && ( {error && <div className="error-message">{error}</div>}
<div className="error-message">{error}</div>
)}
{loading ? ( {loading ? (
<p className="muted">Loading instances...</p> <p className="muted">Loading instances...</p>
@@ -194,7 +210,11 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
{instances.map((instance) => ( {instances.map((instance) => (
<div key={instance.id} className="instance-card"> <div key={instance.id} className="instance-card">
<div className="instance-info"> <div className="instance-info">
<div className="instance-name">{instance.display_name || instance.tool_type_name || "Unnamed Instance"}</div> <div className="instance-name">
{instance.display_name ||
instance.tool_type_name ||
"Unnamed Instance"}
</div>
<div className="instance-meta"> <div className="instance-meta">
<span <span
className="status-dot" className="status-dot"
@@ -210,10 +230,16 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
</div> </div>
</div> </div>
<div className="instance-actions"> <div className="instance-actions">
{instance.status === "running" && instance.url && instance.tool_type_interfaces.includes("web") && ( {instance.status === "running" &&
instance.url &&
instance.tool_type_interfaces.includes("web") && (
<> <>
<a <a
href={instance.url.startsWith("http") ? instance.url : `${API_BASE_URL}${instance.url}`} href={
instance.url.startsWith("http")
? instance.url
: `${API_BASE_URL}${instance.url}`
}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="secondary-button small" className="secondary-button small"
@@ -234,10 +260,13 @@ export const InstanceList = ({ projectId, repoId, toolTypes }: InstanceListProps
)} )}
</> </>
)} )}
{instance.status === "running" && instance.tool_type_interfaces.includes("terminal") && ( {instance.status === "running" &&
instance.tool_type_interfaces.includes("terminal") && (
<button <button
className="secondary-button small" className="secondary-button small"
onClick={() => navigate(`/instances/${instance.id}/terminal`)} onClick={() =>
navigate(`/instances/${instance.id}/terminal`)
}
type="button" type="button"
> >
<Icon name="terminal" size="sm" /> <Icon name="terminal" size="sm" />
@@ -36,7 +36,8 @@ export const RepositoriesSettingsTab: React.FC = () => {
const handleDelete = async (repoId: string) => { const handleDelete = async (repoId: string) => {
if (!projectId) return; if (!projectId) return;
if (!window.confirm("Are you sure you want to delete this repository?")) return; if (!window.confirm("Are you sure you want to delete this repository?"))
return;
try { try {
await deleteRepository(projectId, repoId); await deleteRepository(projectId, repoId);
setRepositories((current) => current.filter((r) => r.id !== repoId)); setRepositories((current) => current.filter((r) => r.id !== repoId));
@@ -1,11 +1,19 @@
import { useEffect, useRef, useState } from "react"; 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 { createRepository, parseGitUrl } from "../api/git_repositories";
import { Icon } from "./icon"; import { Icon } from "./icon";
type CreateMode = "clone" | "blank"; type CreateMode = "clone" | "blank";
type UrlValidationStatus = "idle" | "validating" | "valid" | "needs-parsing" | "invalid"; type UrlValidationStatus =
| "idle"
| "validating"
| "valid"
| "needs-parsing"
| "invalid";
interface RepositoryCreateDialogProps { interface RepositoryCreateDialogProps {
projectId: string; projectId: string;
@@ -15,7 +23,13 @@ interface RepositoryCreateDialogProps {
onCreated: () => Promise<void> | void; onCreated: () => Promise<void> | void;
} }
export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCreated }: RepositoryCreateDialogProps) => { export const RepositoryCreateDialog = ({
projectId,
open,
title,
onClose,
onCreated,
}: RepositoryCreateDialogProps) => {
const [createMode, setCreateMode] = useState<CreateMode>("clone"); const [createMode, setCreateMode] = useState<CreateMode>("clone");
const [formName, setFormName] = useState(""); const [formName, setFormName] = useState("");
const [owner, setOwner] = useState(""); const [owner, setOwner] = useState("");
@@ -129,7 +143,9 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
} catch (error: unknown) { } catch (error: unknown) {
const response = error as { response?: { data?: { detail?: string } } }; const response = error as { response?: { data?: { detail?: string } } };
const detail = response.response?.data?.detail; const detail = response.response?.data?.detail;
setFormError(typeof detail === "string" ? detail : "Failed to create repository"); setFormError(
typeof detail === "string" ? detail : "Failed to create repository",
);
} }
}; };
@@ -161,7 +177,8 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
<div className="dialog"> <div className="dialog">
<h3>{title}</h3> <h3>{title}</h3>
<p className="muted"> <p className="muted">
Clone an existing repository from git.commumedia.org, or create a blank bare repo here. Clone an existing repository from git.commumedia.org, or create a
blank bare repo here.
</p> </p>
<form onSubmit={handleSubmit} className="stack"> <form onSubmit={handleSubmit} className="stack">
<div className="form-field"> <div className="form-field">
@@ -213,7 +230,10 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
placeholder="repo-name" placeholder="repo-name"
/> />
</label> </label>
<p className="muted">SSH target: git@git.commumedia.org:{owner || "owner"}/{repoName || "repo"}.git</p> <p className="muted">
SSH target: git@git.commumedia.org:{owner || "owner"}/
{repoName || "repo"}.git
</p>
<button <button
type="button" type="button"
className="secondary-button small" className="secondary-button small"
@@ -234,20 +254,26 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
className={getUrlInputClass()} className={getUrlInputClass()}
/> />
{urlValidation.status === "validating" && ( {urlValidation.status === "validating" && (
<span className="validation-status validating">Validating...</span> <span className="validation-status validating">
Validating...
</span>
)} )}
{urlValidation.status === "valid" && ( {urlValidation.status === "valid" && (
<span className="validation-status valid"> <span className="validation-status valid">
<Icon name="success" size="sm" /> Valid git URL <Icon name="success" size="sm" /> Valid git URL
</span> </span>
)} )}
{urlValidation.status === "needs-parsing" && urlValidation.result && ( {urlValidation.status === "needs-parsing" &&
urlValidation.result && (
<div className="url-suggestion"> <div className="url-suggestion">
<span className="validation-status warning"> <span className="validation-status warning">
<Icon name="warning" size="sm" /> This looks like a browser URL <Icon name="warning" size="sm" /> This looks like a
browser URL
</span> </span>
<div className="suggestion-actions"> <div className="suggestion-actions">
<span className="suggested-url">Suggested: {urlValidation.result.base_url}</span> <span className="suggested-url">
Suggested: {urlValidation.result.base_url}
</span>
<button <button
type="button" type="button"
className="secondary-button small" className="secondary-button small"
@@ -278,13 +304,19 @@ export const RepositoryCreateDialog = ({ projectId, open, title, onClose, onCrea
</div> </div>
)} )}
<div className="dialog-actions"> <div className="dialog-actions">
<button className="secondary-button" onClick={handleClose} type="button"> <button
className="secondary-button"
onClick={handleClose}
type="button"
>
<Icon name="cancel" size="sm" /> <Icon name="cancel" size="sm" />
Cancel Cancel
</button> </button>
<button className="primary-button" type="submit"> <button className="primary-button" type="submit">
<Icon name="add" size="sm" /> <Icon name="add" size="sm" />
{createMode === "clone" ? "Clone Repository" : "Create Blank Repository"} {createMode === "clone"
? "Clone Repository"
: "Create Blank Repository"}
</button> </button>
</div> </div>
</form> </form>
+179 -37
View File
@@ -2,7 +2,14 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { getDashboardSummary, type DashboardSummary } from "../api/dashboard"; 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 { listProjects } from "../api/projects";
import { listRepositories } from "../api/git_repositories"; import { listRepositories } from "../api/git_repositories";
import { listToolTypes } from "../api/tool_types"; import { listToolTypes } from "../api/tool_types";
@@ -35,14 +42,17 @@ export const HomePage = () => {
const [selectedRepo, setSelectedRepo] = useState(""); const [selectedRepo, setSelectedRepo] = useState("");
const [selectedToolType, setSelectedToolType] = useState(""); const [selectedToolType, setSelectedToolType] = useState("");
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">("idle"); const [saveState, setSaveState] = useState<"idle" | "saving" | "error">(
"idle",
);
const [actionBusy, setActionBusy] = useState<string | null>(null); const [actionBusy, setActionBusy] = useState<string | null>(null);
const safeSessions = Array.isArray(sessions) ? sessions : []; const safeSessions = Array.isArray(sessions) ? sessions : [];
const loadHome = useCallback(async () => { const loadHome = useCallback(async () => {
setStatus("loading"); setStatus("loading");
try { try {
const [dashboard, sessionData, projectData, toolTypeData] = await Promise.all([ const [dashboard, sessionData, projectData, toolTypeData] =
await Promise.all([
getDashboardSummary(), getDashboardSummary(),
getUserSessions(), getUserSessions(),
listProjects(), listProjects(),
@@ -81,13 +91,19 @@ export const HomePage = () => {
}, [selectedProject]); }, [selectedProject]);
const activeSessions = useMemo( const activeSessions = useMemo(
() => safeSessions.filter((session) => ["running", "building", "pending"].includes(session.status)), () =>
[safeSessions] safeSessions.filter((session) =>
["running", "building", "pending"].includes(session.status),
),
[safeSessions],
); );
const recentSessions = useMemo( const recentSessions = useMemo(
() => safeSessions.filter((session) => ["stopped", "error"].includes(session.status)).slice(0, 5), () =>
[safeSessions] safeSessions
.filter((session) => ["stopped", "error"].includes(session.status))
.slice(0, 5),
[safeSessions],
); );
const handleCreate = async (event: React.FormEvent) => { const handleCreate = async (event: React.FormEvent) => {
@@ -96,7 +112,12 @@ export const HomePage = () => {
setSaveState("saving"); setSaveState("saving");
try { try {
const instance = await createInstance(selectedProject, selectedRepo, selectedToolType, displayName || undefined); const instance = await createInstance(
selectedProject,
selectedRepo,
selectedToolType,
displayName || undefined,
);
await startInstance(selectedProject, selectedRepo, instance.id); await startInstance(selectedProject, selectedRepo, instance.id);
await updateUserConfig({ last_session_id: instance.id }); await updateUserConfig({ last_session_id: instance.id });
setDisplayName(""); setDisplayName("");
@@ -135,7 +156,11 @@ export const HomePage = () => {
const handleDelete = async (session: SessionView) => { const handleDelete = async (session: SessionView) => {
setActionBusy(session.id); setActionBusy(session.id);
try { try {
await deleteInstance(session.project_id, session.repository_id, session.id); await deleteInstance(
session.project_id,
session.repository_id,
session.id,
);
await loadHome(); await loadHome();
} finally { } finally {
setActionBusy(null); setActionBusy(null);
@@ -145,7 +170,11 @@ export const HomePage = () => {
const handleRecreateTunnel = async (session: SessionView) => { const handleRecreateTunnel = async (session: SessionView) => {
setActionBusy(session.id); setActionBusy(session.id);
try { try {
await recreateInstanceTunnel(session.project_id, session.repository_id, session.id); await recreateInstanceTunnel(
session.project_id,
session.repository_id,
session.id,
);
await loadHome(); await loadHome();
} finally { } finally {
setActionBusy(null); setActionBusy(null);
@@ -158,11 +187,26 @@ export const HomePage = () => {
<div className="stack-sm"> <div className="stack-sm">
<p className="eyebrow">Workspace overview</p> <p className="eyebrow">Workspace overview</p>
<h1>Home</h1> <h1>Home</h1>
<p className="muted">Open sessions, available projects, and the fastest path back into work.</p> <p className="muted">
Open sessions, available projects, and the fastest path back into
work.
</p>
</div> </div>
<div className="home-hero-actions"> <div className="home-hero-actions">
<button className="primary-button" type="button" onClick={() => navigate("/projects")}>New Project</button> <button
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>Settings</button> className="primary-button"
type="button"
onClick={() => navigate("/projects")}
>
New Project
</button>
<button
className="secondary-button"
type="button"
onClick={() => navigate("/settings")}
>
Settings
</button>
</div> </div>
</header> </header>
@@ -171,7 +215,11 @@ export const HomePage = () => {
{status === "error" && ( {status === "error" && (
<div className="card stack"> <div className="card stack">
<p>Unable to load your workspace overview.</p> <p>Unable to load your workspace overview.</p>
<button className="secondary-button" type="button" onClick={() => void loadHome()}> <button
className="secondary-button"
type="button"
onClick={() => void loadHome()}
>
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
Retry Retry
</button> </button>
@@ -211,25 +259,48 @@ export const HomePage = () => {
<div className="stack-sm"> <div className="stack-sm">
<div className="row row-tight"> <div className="row row-tight">
<h3>{session.display_name}</h3> <h3>{session.display_name}</h3>
<span className={`status-badge ${session.status}`}>{session.status}</span> <span className={`status-badge ${session.status}`}>
{session.status}
</span>
</div> </div>
<p className="muted">{session.project_name} · {session.repository_name}</p> <p className="muted">
{session.project_name} · {session.repository_name}
</p>
<p className="muted">{session.tool_type_name}</p> <p className="muted">{session.tool_type_name}</p>
</div> </div>
<div className="session-actions"> <div className="session-actions">
<button className="secondary-button small" type="button" onClick={() => handleOpen(session)}> <button
className="secondary-button small"
type="button"
onClick={() => handleOpen(session)}
>
<Icon name="external" size="sm" /> <Icon name="external" size="sm" />
Open Open
</button> </button>
<button className="ghost-button small" type="button" onClick={() => void handleRecreateTunnel(session)} disabled={actionBusy === session.id}> <button
className="ghost-button small"
type="button"
onClick={() => void handleRecreateTunnel(session)}
disabled={actionBusy === session.id}
>
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
Tunnel Tunnel
</button> </button>
<button className="ghost-button small" type="button" onClick={() => void handleStop(session)} disabled={actionBusy === session.id}> <button
className="ghost-button small"
type="button"
onClick={() => void handleStop(session)}
disabled={actionBusy === session.id}
>
<Icon name="stop" size="sm" /> <Icon name="stop" size="sm" />
Stop Stop
</button> </button>
<button className="ghost-button small danger-text" type="button" onClick={() => void handleDelete(session)} disabled={actionBusy === session.id}> <button
className="ghost-button small danger-text"
type="button"
onClick={() => void handleDelete(session)}
disabled={actionBusy === session.id}
>
<Icon name="delete" size="sm" /> <Icon name="delete" size="sm" />
Delete Delete
</button> </button>
@@ -246,19 +317,34 @@ export const HomePage = () => {
<p className="eyebrow">Available projects</p> <p className="eyebrow">Available projects</p>
<h2>{projects.length}</h2> <h2>{projects.length}</h2>
</div> </div>
<button className="secondary-button" type="button" onClick={() => navigate("/projects")}>View all</button> <button
className="secondary-button"
type="button"
onClick={() => navigate("/projects")}
>
View all
</button>
</div> </div>
{projects.length === 0 ? ( {projects.length === 0 ? (
<p className="muted">No projects yet.</p> <p className="muted">No projects yet.</p>
) : ( ) : (
<div className="home-project-grid"> <div className="home-project-grid">
{projects.map((project) => ( {projects.map((project) => (
<article className="card project-card home-project-card" key={project.id}> <article
className="card project-card home-project-card"
key={project.id}
>
<div className="stack-sm"> <div className="stack-sm">
<h3>{project.name}</h3> <h3>{project.name}</h3>
{project.description && <p className="muted">{project.description}</p>} {project.description && (
<p className="muted">{project.description}</p>
)}
</div> </div>
<button className="ghost-button small" type="button" onClick={() => navigate(`/projects/${project.id}`)}> <button
className="ghost-button small"
type="button"
onClick={() => navigate(`/projects/${project.id}`)}
>
Open Workspace Open Workspace
</button> </button>
</article> </article>
@@ -278,35 +364,81 @@ export const HomePage = () => {
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
Project Project
<select value={selectedProject} onChange={(event) => { setSelectedProject(event.target.value); setSelectedRepo(""); }}> <select
value={selectedProject}
onChange={(event) => {
setSelectedProject(event.target.value);
setSelectedRepo("");
}}
>
<option value="">Select project...</option> <option value="">Select project...</option>
{projects.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)} {projects.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select> </select>
</label> </label>
<label className="form-field"> <label className="form-field">
Repository Repository
<select value={selectedRepo} onChange={(event) => setSelectedRepo(event.target.value)} disabled={!selectedProject}> <select
value={selectedRepo}
onChange={(event) => setSelectedRepo(event.target.value)}
disabled={!selectedProject}
>
<option value="">Select repository...</option> <option value="">Select repository...</option>
{repositories.map((repo) => <option key={repo.id} value={repo.id}>{repo.name}</option>)} {repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.name}
</option>
))}
</select> </select>
</label> </label>
<label className="form-field"> <label className="form-field">
Tool type Tool type
<select value={selectedToolType} onChange={(event) => setSelectedToolType(event.target.value)}> <select
value={selectedToolType}
onChange={(event) =>
setSelectedToolType(event.target.value)
}
>
<option value="">Select tool...</option> <option value="">Select tool...</option>
{toolTypes.map((tool) => <option key={tool.id} value={tool.id}>{tool.display_name}</option>)} {toolTypes.map((tool) => (
<option key={tool.id} value={tool.id}>
{tool.display_name}
</option>
))}
</select> </select>
</label> </label>
</div> </div>
<label className="form-field"> <label className="form-field">
Display name Display name
<input type="text" value={displayName} onChange={(event) => setDisplayName(event.target.value)} placeholder="My Development Environment" /> <input
type="text"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
placeholder="My Development Environment"
/>
</label> </label>
<div className="form-actions"> <div className="form-actions">
<button className="primary-button" type="submit" disabled={saveState === "saving"}> <button
{saveState === "saving" ? <><Icon name="loading" size="sm" /> Creating...</> : <><Icon name="add" size="sm" /> Create Session</>} className="primary-button"
type="submit"
disabled={saveState === "saving"}
>
{saveState === "saving" ? (
<>
<Icon name="loading" size="sm" /> Creating...
</>
) : (
<>
<Icon name="add" size="sm" /> Create Session
</>
)}
</button> </button>
{saveState === "error" && <span className="error-text">Failed to create session</span>} {saveState === "error" && (
<span className="error-text">Failed to create session</span>
)}
</div> </div>
</form> </form>
</section> </section>
@@ -323,10 +455,20 @@ export const HomePage = () => {
{recentSessions.map((session) => ( {recentSessions.map((session) => (
<article className="recent-session-item" key={session.id}> <article className="recent-session-item" key={session.id}>
<div className="recent-session-info"> <div className="recent-session-info">
<span className="recent-session-name">{session.display_name}</span> <span className="recent-session-name">
<span className="muted">{session.project_name} · {session.tool_type_name}</span> {session.display_name}
</span>
<span className="muted">
{session.project_name} · {session.tool_type_name}
</span>
</div> </div>
<button className="ghost-button small" type="button" onClick={() => handleOpen(session)}>Open</button> <button
className="ghost-button small"
type="button"
onClick={() => handleOpen(session)}
>
Open
</button>
</article> </article>
))} ))}
</div> </div>
+21 -8
View File
@@ -2,10 +2,7 @@ import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import type { GitRepository } from "../types/git-repository"; import type { GitRepository } from "../types/git-repository";
import { import { deleteRepository, listRepositories } from "../api/git_repositories";
deleteRepository,
listRepositories,
} from "../api/git_repositories";
import { Icon } from "../components/icon"; import { Icon } from "../components/icon";
import { RepositoryCreateDialog } from "../components/repository-create-dialog"; import { RepositoryCreateDialog } from "../components/repository-create-dialog";
@@ -53,7 +50,11 @@ export const GitRepositoriesPage = () => {
<section className="stack"> <section className="stack">
<div className="page-header"> <div className="page-header">
<h1>Repositories</h1> <h1>Repositories</h1>
<button className="primary-button" onClick={() => setShowCreate(true)} type="button"> <button
className="primary-button"
onClick={() => setShowCreate(true)}
type="button"
>
<Icon name="add" size="sm" /> <Icon name="add" size="sm" />
New Repository New Repository
</button> </button>
@@ -64,14 +65,22 @@ export const GitRepositoriesPage = () => {
{status === "error" && ( {status === "error" && (
<div className="card stack"> <div className="card stack">
<p>Failed to load repositories</p> <p>Failed to load repositories</p>
<button className="secondary-button" onClick={() => void loadRepositories()} type="button"> <button
className="secondary-button"
onClick={() => void loadRepositories()}
type="button"
>
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
Retry Retry
</button> </button>
</div> </div>
)} )}
{isEmpty && <p className="muted">No repositories yet. Create your first repository above.</p>} {isEmpty && (
<p className="muted">
No repositories yet. Create your first repository above.
</p>
)}
{status === "ready" && repositories.length > 0 && ( {status === "ready" && repositories.length > 0 && (
<div className="repository-list"> <div className="repository-list">
@@ -87,7 +96,11 @@ export const GitRepositoriesPage = () => {
<div className="repository-actions"> <div className="repository-actions">
<button <button
className="secondary-button small" className="secondary-button small"
onClick={() => navigate(`/projects/${projectId}/repositories/${repo.id}/history`)} onClick={() =>
navigate(
`/projects/${projectId}/repositories/${repo.id}/history`,
)
}
type="button" type="button"
> >
History History
+23 -15
View File
@@ -48,7 +48,7 @@ export const RepoWorkspace = () => {
const [project, setProject] = useState<Project | null>(null); const [project, setProject] = useState<Project | null>(null);
const [repositories, setRepositories] = useState<GitRepository[]>([]); const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [selectedRepoId, setSelectedRepoId] = useState<string | null>( const [selectedRepoId, setSelectedRepoId] = useState<string | null>(
searchParams.get("repo") searchParams.get("repo"),
); );
const [branches, setBranches] = useState<string[]>([]); const [branches, setBranches] = useState<string[]>([]);
const [currentBranch, setCurrentBranch] = useState<string>("main"); const [currentBranch, setCurrentBranch] = useState<string>("main");
@@ -95,9 +95,11 @@ export const RepoWorkspace = () => {
if (!projectId || !selectedRepoId) return; if (!projectId || !selectedRepoId) return;
try { try {
const response = await apiClient.get( const response = await apiClient.get(
`/projects/${projectId}/repositories/${selectedRepoId}/branches` `/projects/${projectId}/repositories/${selectedRepoId}/branches`,
);
const branchList = response.data.branches.map(
(b: { name: string }) => b.name,
); );
const branchList = response.data.branches.map((b: { name: string }) => b.name);
setBranches(branchList); setBranches(branchList);
const defaultBranch = response.data.default_branch; const defaultBranch = response.data.default_branch;
if (defaultBranch) { if (defaultBranch) {
@@ -152,15 +154,10 @@ export const RepoWorkspace = () => {
return ( return (
<section className="repo-workspace"> <section className="repo-workspace">
{project && ( {project && (
<WorkspaceHeader <WorkspaceHeader project={project} currentRepo={selectedRepo || null} />
project={project}
currentRepo={selectedRepo || null}
/>
)} )}
{status === "loading" && ( {status === "loading" && <p className="muted">Loading repositories...</p>}
<p className="muted">Loading repositories...</p>
)}
{status === "error" && ( {status === "error" && (
<div className="card stack"> <div className="card stack">
@@ -245,7 +242,9 @@ export const RepoWorkspace = () => {
untracked={gitStatus.untracked} untracked={gitStatus.untracked}
onCommit={() => { onCommit={() => {
void loadGitStatus(); void loadGitStatus();
window.dispatchEvent(new CustomEvent("refresh-file-tree")); window.dispatchEvent(
new CustomEvent("refresh-file-tree"),
);
}} }}
/> />
)} )}
@@ -301,7 +300,7 @@ const FileBrowser = ({
branch, branch,
path, path,
}, },
} },
); );
setEntries(response.data.entries || []); setEntries(response.data.entries || []);
} catch { } catch {
@@ -361,7 +360,11 @@ const FileBrowser = ({
return ( return (
<div className="file-tree"> <div className="file-tree">
{path && ( {path && (
<button className="tree-entry tree-up" onClick={navigateUp} type="button"> <button
className="tree-entry tree-up"
onClick={navigateUp}
type="button"
>
<Icon name="folder" size="sm" /> .. <Icon name="folder" size="sm" /> ..
</button> </button>
)} )}
@@ -369,7 +372,8 @@ const FileBrowser = ({
<p className="muted">No files in this repository yet.</p> <p className="muted">No files in this repository yet.</p>
)} )}
{entries.map((entry) => { {entries.map((entry) => {
const fileStatus = entry.type === "file" ? getFileStatus(entry.path) : null; const fileStatus =
entry.type === "file" ? getFileStatus(entry.path) : null;
return ( return (
<button <button
key={entry.path} key={entry.path}
@@ -377,7 +381,11 @@ const FileBrowser = ({
onClick={() => handleEntryClick(entry)} onClick={() => handleEntryClick(entry)}
type="button" type="button"
> >
<Icon name={entry.type === "directory" ? "folder" : "file"} size="sm" /> {entry.name} <Icon
name={entry.type === "directory" ? "folder" : "file"}
size="sm"
/>{" "}
{entry.name}
{fileStatus && ( {fileStatus && (
<span className={`file-status-indicator ${fileStatus}`}> <span className={`file-status-indicator ${fileStatus}`}>
{fileStatus === "modified" && "M"} {fileStatus === "modified" && "M"}
+97 -32
View File
@@ -42,7 +42,12 @@ export const SessionsPage = () => {
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null); const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
const [stopConfirmId, setStopConfirmId] = useState<string | null>(null); const [stopConfirmId, setStopConfirmId] = useState<string | null>(null);
const [tunnelHealth, setTunnelHealth] = useState<Record<string, { healthy: boolean; status_code: number | null; error?: string }>>({}); const [tunnelHealth, setTunnelHealth] = useState<
Record<
string,
{ healthy: boolean; status_code: number | null; error?: string }
>
>({});
const [recreatingId, setRecreatingId] = useState<string | null>(null); const [recreatingId, setRecreatingId] = useState<string | null>(null);
const loadSessions = useCallback(async () => { const loadSessions = useCallback(async () => {
@@ -92,14 +97,14 @@ export const SessionsPage = () => {
useEffect(() => { useEffect(() => {
const checkHealth = async () => { const checkHealth = async () => {
const runningSessions = sessions.filter( const runningSessions = sessions.filter(
(s) => s.status === "running" && s.url (s) => s.status === "running" && s.url,
); );
for (const session of runningSessions) { for (const session of runningSessions) {
try { try {
const health = await checkInstanceHealth( const health = await checkInstanceHealth(
session.project_id, session.project_id,
session.repository_id, session.repository_id,
session.id session.id,
); );
setTunnelHealth((prev) => ({ setTunnelHealth((prev) => ({
...prev, ...prev,
@@ -108,7 +113,11 @@ export const SessionsPage = () => {
} catch { } catch {
setTunnelHealth((prev) => ({ setTunnelHealth((prev) => ({
...prev, ...prev,
[session.id]: { healthy: false, status_code: null, error: "check failed" }, [session.id]: {
healthy: false,
status_code: null,
error: "check failed",
},
})); }));
} }
} }
@@ -137,18 +146,24 @@ export const SessionsPage = () => {
}, [selectedProject]); }, [selectedProject]);
const activeSessions = useMemo( const activeSessions = useMemo(
() => sessions.filter((s) => ["running", "building", "pending"].includes(s.status)), () =>
[sessions] sessions.filter((s) =>
["running", "building", "pending"].includes(s.status),
),
[sessions],
); );
const recentSessions = useMemo( const recentSessions = useMemo(
() => sessions.filter((s) => ["stopped", "error"].includes(s.status)).slice(0, 5), () =>
[sessions] sessions
.filter((s) => ["stopped", "error"].includes(s.status))
.slice(0, 5),
[sessions],
); );
const lastSession = useMemo( const lastSession = useMemo(
() => sessions.find((s) => s.id === lastSessionId) ?? null, () => sessions.find((s) => s.id === lastSessionId) ?? null,
[sessions, lastSessionId] [sessions, lastSessionId],
); );
const handleCreate = async (e: React.FormEvent) => { const handleCreate = async (e: React.FormEvent) => {
@@ -166,7 +181,7 @@ export const SessionsPage = () => {
selectedProject, selectedProject,
selectedRepo, selectedRepo,
selectedToolType, selectedToolType,
displayName || undefined displayName || undefined,
); );
// Auto-start the instance // Auto-start the instance
@@ -185,7 +200,11 @@ export const SessionsPage = () => {
} }
}; };
const handleStop = async (sessionId: string, projectId: string, repoId: string) => { const handleStop = async (
sessionId: string,
projectId: string,
repoId: string,
) => {
try { try {
await stopInstance(projectId, repoId, sessionId); await stopInstance(projectId, repoId, sessionId);
setStopConfirmId(null); setStopConfirmId(null);
@@ -195,7 +214,11 @@ export const SessionsPage = () => {
} }
}; };
const handleDelete = async (sessionId: string, projectId: string, repoId: string) => { const handleDelete = async (
sessionId: string,
projectId: string,
repoId: string,
) => {
try { try {
await deleteInstance(projectId, repoId, sessionId); await deleteInstance(projectId, repoId, sessionId);
setDeleteConfirmId(null); setDeleteConfirmId(null);
@@ -212,7 +235,7 @@ export const SessionsPage = () => {
await recreateInstanceTunnel( await recreateInstanceTunnel(
session.project_id, session.project_id,
session.repository_id, session.repository_id,
session.id session.id,
); );
// Refresh sessions to get new URL // Refresh sessions to get new URL
await loadSessions(); await loadSessions();
@@ -225,7 +248,7 @@ export const SessionsPage = () => {
const handleOpen = (session: Session) => { const handleOpen = (session: Session) => {
if (session.url) { if (session.url) {
window.open(session.url, '_blank', 'noopener,noreferrer'); window.open(session.url, "_blank", "noopener,noreferrer");
} else if (session.tool_type_interfaces?.includes("terminal")) { } else if (session.tool_type_interfaces?.includes("terminal")) {
navigate(`/instances/${session.id}/terminal`); navigate(`/instances/${session.id}/terminal`);
} else { } else {
@@ -253,7 +276,11 @@ export const SessionsPage = () => {
{status === "error" && ( {status === "error" && (
<div className="card stack"> <div className="card stack">
<p>Failed to load sessions</p> <p>Failed to load sessions</p>
<button className="secondary-button" onClick={() => void loadSessions()} type="button"> <button
className="secondary-button"
onClick={() => void loadSessions()}
type="button"
>
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
Retry Retry
</button> </button>
@@ -268,18 +295,29 @@ export const SessionsPage = () => {
<h2>Last Session</h2> <h2>Last Session</h2>
<div className="card last-session-card"> <div className="card last-session-card">
<div className="last-session-info"> <div className="last-session-info">
<h3>{lastSession.display_name || lastSession.tool_type_name || "Unnamed Session"}</h3> <h3>
{lastSession.display_name ||
lastSession.tool_type_name ||
"Unnamed Session"}
</h3>
<p className="muted"> <p className="muted">
{lastSession.tool_type_name} · {lastSession.project_name} · {lastSession.repository_name} {lastSession.tool_type_name} · {lastSession.project_name} ·{" "}
{lastSession.repository_name}
</p> </p>
{lastSession.url && ( {lastSession.url && (
<p className="session-url"> <p className="session-url">
<a href={lastSession.url} target="_blank" rel="noopener noreferrer"> <a
href={lastSession.url}
target="_blank"
rel="noopener noreferrer"
>
{lastSession.url} {lastSession.url}
</a> </a>
</p> </p>
)} )}
<span className={`status-badge ${lastSession.status}`}>{lastSession.status}</span> <span className={`status-badge ${lastSession.status}`}>
{lastSession.status}
</span>
</div> </div>
<div className="last-session-actions"> <div className="last-session-actions">
{lastSession.url ? ( {lastSession.url ? (
@@ -293,7 +331,11 @@ export const SessionsPage = () => {
Open Open
</a> </a>
) : ( ) : (
<button className="primary-button" onClick={handleResumeLast} type="button"> <button
className="primary-button"
onClick={handleResumeLast}
type="button"
>
<Icon name="play" size="sm" /> <Icon name="play" size="sm" />
Resume Resume
</button> </button>
@@ -318,20 +360,33 @@ export const SessionsPage = () => {
{activeSessions.map((session) => ( {activeSessions.map((session) => (
<div className="card session-card" key={session.id}> <div className="card session-card" key={session.id}>
<div className="session-info"> <div className="session-info">
<h4>{session.display_name || session.tool_type_name || "Unnamed Session"}</h4> <h4>
{session.display_name ||
session.tool_type_name ||
"Unnamed Session"}
</h4>
<p className="muted"> <p className="muted">
{session.tool_type_name} · {session.project_name} {session.tool_type_name} · {session.project_name}
</p> </p>
{session.url && ( {session.url && (
<p className="session-url"> <p className="session-url">
<a href={session.url} target="_blank" rel="noopener noreferrer"> <a
href={session.url}
target="_blank"
rel="noopener noreferrer"
>
{session.url} {session.url}
</a> </a>
</p> </p>
)} )}
<span className={`status-badge ${session.status}`}>{session.status}</span> <span className={`status-badge ${session.status}`}>
{tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && ( {session.status}
<span className="status-badge error">tunnel error</span> </span>
{tunnelHealth[session.id] &&
!tunnelHealth[session.id].healthy && (
<span className="status-badge error">
tunnel error
</span>
)} )}
</div> </div>
<div className="session-actions"> <div className="session-actions">
@@ -355,7 +410,8 @@ export const SessionsPage = () => {
Open Open
</button> </button>
)} )}
{tunnelHealth[session.id] && !tunnelHealth[session.id].healthy && ( {tunnelHealth[session.id] &&
!tunnelHealth[session.id].healthy && (
<button <button
className="secondary-button small" className="secondary-button small"
onClick={() => void handleRecreateTunnel(session)} onClick={() => void handleRecreateTunnel(session)}
@@ -363,7 +419,9 @@ export const SessionsPage = () => {
disabled={recreatingId === session.id} disabled={recreatingId === session.id}
> >
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
{recreatingId === session.id ? "Recreating..." : "Recreate Tunnel"} {recreatingId === session.id
? "Recreating..."
: "Recreate Tunnel"}
</button> </button>
)} )}
{stopConfirmId === session.id ? ( {stopConfirmId === session.id ? (
@@ -375,7 +433,7 @@ export const SessionsPage = () => {
void handleStop( void handleStop(
session.id, session.id,
session.project_id, session.project_id,
session.repository_id session.repository_id,
) )
} }
type="button" type="button"
@@ -408,7 +466,7 @@ export const SessionsPage = () => {
void handleDelete( void handleDelete(
session.id, session.id,
session.project_id, session.project_id,
session.repository_id session.repository_id,
) )
} }
type="button" type="button"
@@ -447,7 +505,11 @@ export const SessionsPage = () => {
{recentSessions.map((session) => ( {recentSessions.map((session) => (
<div className="recent-session-item" key={session.id}> <div className="recent-session-item" key={session.id}>
<div className="recent-session-info"> <div className="recent-session-info">
<span className="recent-session-name">{session.display_name || session.tool_type_name || "Unnamed Session"}</span> <span className="recent-session-name">
{session.display_name ||
session.tool_type_name ||
"Unnamed Session"}
</span>
<span className="muted"> <span className="muted">
{session.tool_type_name} · {session.project_name} {session.tool_type_name} · {session.project_name}
</span> </span>
@@ -479,7 +541,7 @@ export const SessionsPage = () => {
void handleDelete( void handleDelete(
session.id, session.id,
session.project_id, session.project_id,
session.repository_id session.repository_id,
) )
} }
type="button" type="button"
@@ -513,7 +575,10 @@ export const SessionsPage = () => {
{/* Create Session */} {/* Create Session */}
<div className="create-session-section"> <div className="create-session-section">
<h2>Create New Session</h2> <h2>Create New Session</h2>
<form onSubmit={handleCreate} className="card stack create-session-form"> <form
onSubmit={handleCreate}
className="card stack create-session-form"
>
<div className="form-row"> <div className="form-row">
<label className="form-field"> <label className="form-field">
Project Project
+50 -14
View File
@@ -28,7 +28,9 @@ export const ToolConfigsPage = () => {
config_type: "env", config_type: "env",
file_path: "", file_path: "",
}); });
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle"); const [saveStatus, setSaveStatus] = useState<
"idle" | "saving" | "saved" | "error"
>("idle");
const loadData = useCallback(async () => { const loadData = useCallback(async () => {
try { try {
@@ -60,7 +62,8 @@ export const ToolConfigsPage = () => {
key: formData.key, key: formData.key,
value: formData.value, value: formData.value,
config_type: formData.config_type, config_type: formData.config_type,
file_path: formData.config_type === "file" ? formData.file_path : undefined, file_path:
formData.config_type === "file" ? formData.file_path : undefined,
}; };
if (editingConfig) { if (editingConfig) {
@@ -102,7 +105,7 @@ export const ToolConfigsPage = () => {
}; };
const filteredConfigs = configs.filter( const filteredConfigs = configs.filter(
(c) => c.tool_type_id === selectedToolType (c) => c.tool_type_id === selectedToolType,
); );
const selectedTool = toolTypes.find((t) => t.id === selectedToolType); const selectedTool = toolTypes.find((t) => t.id === selectedToolType);
@@ -126,7 +129,11 @@ export const ToolConfigsPage = () => {
</div> </div>
<div className="card stack"> <div className="card stack">
<p>Failed to load configurations</p> <p>Failed to load configurations</p>
<button className="secondary-button" onClick={() => void loadData()} type="button"> <button
className="secondary-button"
onClick={() => void loadData()}
type="button"
>
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
Retry Retry
</button> </button>
@@ -142,7 +149,11 @@ export const ToolConfigsPage = () => {
<p className="eyebrow">Settings</p> <p className="eyebrow">Settings</p>
<h1>Tool Configurations</h1> <h1>Tool Configurations</h1>
</div> </div>
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}> <button
className="secondary-button"
type="button"
onClick={() => navigate("/settings")}
>
Back to settings Back to settings
</button> </button>
<p className="muted"> <p className="muted">
@@ -171,21 +182,30 @@ export const ToolConfigsPage = () => {
</select> </select>
{selectedTool && ( {selectedTool && (
<p className="muted" style={{ marginTop: "0.5rem" }}> <p className="muted" style={{ marginTop: "0.5rem" }}>
Category: {selectedTool.category} · Interfaces: {selectedTool.interfaces?.join(", ")} Category: {selectedTool.category} · Interfaces:{" "}
{selectedTool.interfaces?.join(", ")}
</p> </p>
)} )}
</div> </div>
{/* Config List */} {/* Config List */}
<div className="card stack"> <div className="card stack">
<div className="row" style={{ justifyContent: "space-between", alignItems: "center" }}> <div
className="row"
style={{ justifyContent: "space-between", alignItems: "center" }}
>
<h2>Configuration Variables</h2> <h2>Configuration Variables</h2>
<button <button
className="primary-button small" className="primary-button small"
onClick={() => { onClick={() => {
setShowForm(true); setShowForm(true);
setEditingConfig(null); setEditingConfig(null);
setFormData({ key: "", value: "", config_type: "env", file_path: "" }); setFormData({
key: "",
value: "",
config_type: "env",
file_path: "",
});
}} }}
type="button" type="button"
> >
@@ -210,14 +230,20 @@ export const ToolConfigsPage = () => {
}} }}
> >
<div> <div>
<div className="row" style={{ gap: "0.5rem", alignItems: "center" }}> <div
className="row"
style={{ gap: "0.5rem", alignItems: "center" }}
>
<code style={{ fontWeight: 600 }}>{config.key}</code> <code style={{ fontWeight: 600 }}>{config.key}</code>
<span <span
className="badge" className="badge"
style={{ style={{
fontSize: "0.7rem", fontSize: "0.7rem",
textTransform: "uppercase", textTransform: "uppercase",
background: config.config_type === "env" ? "var(--color-info)" : "var(--color-warning)", background:
config.config_type === "env"
? "var(--color-info)"
: "var(--color-warning)",
color: "white", color: "white",
padding: "0.125rem 0.5rem", padding: "0.125rem 0.5rem",
borderRadius: "9999px", borderRadius: "9999px",
@@ -226,7 +252,10 @@ export const ToolConfigsPage = () => {
{config.config_type} {config.config_type}
</span> </span>
</div> </div>
<p className="muted" style={{ marginTop: "0.25rem", fontSize: "0.875rem" }}> <p
className="muted"
style={{ marginTop: "0.25rem", fontSize: "0.875rem" }}
>
{config.config_type === "file" && config.file_path {config.config_type === "file" && config.file_path
? `File: ${config.file_path}` ? `File: ${config.file_path}`
: "Environment variable"} : "Environment variable"}
@@ -265,7 +294,9 @@ export const ToolConfigsPage = () => {
id="config-key" id="config-key"
type="text" type="text"
value={formData.key} value={formData.key}
onChange={(e) => setFormData({ ...formData, key: e.target.value })} onChange={(e) =>
setFormData({ ...formData, key: e.target.value })
}
placeholder="e.g., OPENAI_API_KEY" placeholder="e.g., OPENAI_API_KEY"
className="form-input" className="form-input"
required required
@@ -309,7 +340,9 @@ export const ToolConfigsPage = () => {
<textarea <textarea
id="config-value" id="config-value"
value={formData.value} value={formData.value}
onChange={(e) => setFormData({ ...formData, value: e.target.value })} onChange={(e) =>
setFormData({ ...formData, value: e.target.value })
}
placeholder={ placeholder={
formData.config_type === "env" formData.config_type === "env"
? "Enter value..." ? "Enter value..."
@@ -321,7 +354,10 @@ export const ToolConfigsPage = () => {
/> />
</div> </div>
<div className="row" style={{ gap: "0.5rem", justifyContent: "flex-end" }}> <div
className="row"
style={{ gap: "0.5rem", justifyContent: "flex-end" }}
>
<button <button
type="button" type="button"
className="secondary-button" className="secondary-button"
+42 -11
View File
@@ -1,7 +1,11 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import type { ToolType, CreateToolTypeRequest, UpdateToolTypeRequest } from "../types/tool-type"; import type {
ToolType,
CreateToolTypeRequest,
UpdateToolTypeRequest,
} from "../types/tool-type";
import { import {
createToolType, createToolType,
deleteToolType, deleteToolType,
@@ -128,7 +132,8 @@ export const ToolTypesPage = () => {
await loadToolTypes(); await loadToolTypes();
} catch (err) { } catch (err) {
const axiosError = err as { response?: { data?: { detail?: string } } }; const axiosError = err as { response?: { data?: { detail?: string } } };
const detail = axiosError?.response?.data?.detail || "Failed to save tool type"; const detail =
axiosError?.response?.data?.detail || "Failed to save tool type";
setFormError(detail); setFormError(detail);
} }
}; };
@@ -171,7 +176,13 @@ export const ToolTypesPage = () => {
<h1>Tool Types</h1> <h1>Tool Types</h1>
</div> </div>
<div className="row"> <div className="row">
<button className="secondary-button" type="button" onClick={() => navigate("/settings")}>Back to settings</button> <button
className="secondary-button"
type="button"
onClick={() => navigate("/settings")}
>
Back to settings
</button>
<button onClick={openCreate}> <button onClick={openCreate}>
<Icon name="add" size="sm" /> <Icon name="add" size="sm" />
Create Tool Type Create Tool Type
@@ -189,18 +200,25 @@ export const ToolTypesPage = () => {
<h3>{toolType.display_name}</h3> <h3>{toolType.display_name}</h3>
{toolType.is_builtin && <span className="badge">Built-in</span>} {toolType.is_builtin && <span className="badge">Built-in</span>}
</div> </div>
<p className="text-secondary">{toolType.description || "No description"}</p> <p className="text-secondary">
{toolType.description || "No description"}
</p>
<div className="tool-type-meta"> <div className="tool-type-meta">
<span>Port: {toolType.default_port || "N/A"}</span> <span>Port: {toolType.default_port || "N/A"}</span>
{toolType.interfaces?.length > 0 && ( {toolType.interfaces?.length > 0 && (
<span>Interfaces: {toolType.interfaces.join(", ")}</span> <span>Interfaces: {toolType.interfaces.join(", ")}</span>
)} )}
{toolType.category && <span>Category: {toolType.category}</span>} {toolType.category && (
<span>Category: {toolType.category}</span>
)}
</div> </div>
<div className="card-actions"> <div className="card-actions">
{!toolType.is_builtin && ( {!toolType.is_builtin && (
<> <>
<button onClick={() => openEdit(toolType)} className="button-secondary"> <button
onClick={() => openEdit(toolType)}
className="button-secondary"
>
<Icon name="edit" size="sm" /> <Icon name="edit" size="sm" />
Edit Edit
</button> </button>
@@ -220,7 +238,10 @@ export const ToolTypesPage = () => {
<div className="dialog"> <div className="dialog">
<p>Delete tool type "{toolType.display_name}"?</p> <p>Delete tool type "{toolType.display_name}"?</p>
<div className="dialog-actions"> <div className="dialog-actions">
<button onClick={() => handleDelete(toolType.id)} className="button-danger"> <button
onClick={() => handleDelete(toolType.id)}
className="button-danger"
>
<Icon name="delete" size="sm" /> <Icon name="delete" size="sm" />
Delete Delete
</button> </button>
@@ -240,7 +261,9 @@ export const ToolTypesPage = () => {
{dialogMode !== "none" && ( {dialogMode !== "none" && (
<div className="dialog-overlay"> <div className="dialog-overlay">
<div className="dialog"> <div className="dialog">
<h2>{dialogMode === "create" ? "Create Tool Type" : "Edit Tool Type"}</h2> <h2>
{dialogMode === "create" ? "Create Tool Type" : "Edit Tool Type"}
</h2>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<label>Name (unique identifier)</label> <label>Name (unique identifier)</label>
@@ -294,7 +317,9 @@ export const ToolTypesPage = () => {
if (e.target.checked) { if (e.target.checked) {
setFormInterfaces([...formInterfaces, "web"]); setFormInterfaces([...formInterfaces, "web"]);
} else { } else {
setFormInterfaces(formInterfaces.filter((i) => i !== "web")); setFormInterfaces(
formInterfaces.filter((i) => i !== "web"),
);
} }
}} }}
/> />
@@ -308,7 +333,9 @@ export const ToolTypesPage = () => {
if (e.target.checked) { if (e.target.checked) {
setFormInterfaces([...formInterfaces, "terminal"]); setFormInterfaces([...formInterfaces, "terminal"]);
} else { } else {
setFormInterfaces(formInterfaces.filter((i) => i !== "terminal")); setFormInterfaces(
formInterfaces.filter((i) => i !== "terminal"),
);
} }
}} }}
/> />
@@ -364,7 +391,11 @@ export const ToolTypesPage = () => {
</> </>
)} )}
</button> </button>
<button type="button" onClick={closeDialog} className="button-secondary"> <button
type="button"
onClick={closeDialog}
className="button-secondary"
>
<Icon name="cancel" size="sm" /> <Icon name="cancel" size="sm" />
Cancel Cancel
</button> </button>
+10 -2
View File
@@ -1,4 +1,10 @@
import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; import {
createContext,
useCallback,
useContext,
useState,
type ReactNode,
} from "react";
import type { Session } from "../types/session"; import type { Session } from "../types/session";
export type { Session } from "../types/session"; export type { Session } from "../types/session";
@@ -8,7 +14,9 @@ interface SessionsContextType {
setAllSessions: (sessions: Session[]) => void; setAllSessions: (sessions: Session[]) => void;
} }
const SessionsContext = createContext<SessionsContextType | undefined>(undefined); const SessionsContext = createContext<SessionsContextType | undefined>(
undefined,
);
export const SessionsProvider = ({ children }: { children: ReactNode }) => { export const SessionsProvider = ({ children }: { children: ReactNode }) => {
const [sessions, setSessions] = useState<Session[]>([]); const [sessions, setSessions] = useState<Session[]>([]);
+4 -1
View File
@@ -5,7 +5,10 @@ export interface ConfigFolder {
description: string | null; description: string | null;
mount_path: string; mount_path: string;
files: Record<string, string>; files: Record<string, string>;
project_overrides: Record<string, { mount_path?: string; files?: Record<string, string> }> | null; project_overrides: Record<
string,
{ mount_path?: string; files?: Record<string, string> }
> | null;
is_active: boolean; is_active: boolean;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
+6 -1
View File
@@ -1,5 +1,10 @@
export type { ApiResponse, PaginatedResponse } from "./api-response"; export type { ApiResponse, PaginatedResponse } from "./api-response";
export type { ConfigFolder, CreateConfigFolderRequest, UpdateConfigFolderRequest, ProjectOverrideRequest } from "./config-folder"; export type {
ConfigFolder,
CreateConfigFolderRequest,
UpdateConfigFolderRequest,
ProjectOverrideRequest,
} from "./config-folder";
export type { export type {
CommitDetail, CommitDetail,
CommitHistoryEntry, CommitHistoryEntry,