fix: add from __future__ import annotations to workspace_manager.py

Fixes NameError: ToolInstance not defined at runtime because
type annotations are evaluated at class definition time.
Deferring annotation evaluation with __future__ annotations
keeps TYPE_CHECKING imports from causing runtime crashes.

Also includes ruff formatting cleanup on workspace-related files.
This commit is contained in:
2026-05-31 23:41:45 +02:00
parent 5bba2bbd92
commit a5d64d1859
15 changed files with 831 additions and 699 deletions
+1
View File
@@ -1684,6 +1684,7 @@ async def start_instance(
repo_path = "" repo_path = ""
if instance.workspace_id: if instance.workspace_id:
from src.models.workspace import Workspace as WorkspaceModel from src.models.workspace import Workspace as WorkspaceModel
workspace = await session.get(WorkspaceModel, instance.workspace_id) workspace = await session.get(WorkspaceModel, instance.workspace_id)
if workspace: if workspace:
repo_path = workspace.path repo_path = workspace.path
+4 -4
View File
@@ -108,7 +108,9 @@ async def create_workspace(
"branch": workspace.branch, "branch": workspace.branch,
"path": workspace.path, "path": workspace.path,
"status": workspace.status, "status": workspace.status,
"created_at": workspace.created_at.isoformat() if workspace.created_at else None, "created_at": workspace.created_at.isoformat()
if workspace.created_at
else None,
} }
@@ -216,9 +218,7 @@ async def delete_workspace(
status_code=409, status_code=409,
detail={ detail={
"message": "Workspace has running tool instances", "message": "Workspace has running tool instances",
"instances": [ "instances": [{"id": str(i.id), "name": i.name} for i in exc.instances],
{"id": str(i.id), "name": i.name} for i in exc.instances
],
}, },
) from exc ) from exc
except Exception as exc: except Exception as exc:
+3 -3
View File
@@ -1,5 +1,7 @@
"""Workspace lifecycle management service.""" """Workspace lifecycle management service."""
from __future__ import annotations
import logging import logging
import os import os
import shutil import shutil
@@ -170,6 +172,4 @@ class WorkspaceManager:
TODO(PR-2): Wire up to actual instance stop/delete logic. TODO(PR-2): Wire up to actual instance stop/delete logic.
For now, this is a placeholder. For now, this is a placeholder.
""" """
logger.warning( logger.warning("Placeholder: stopping and deleting instance %s", instance.id)
"Placeholder: stopping and deleting instance %s", instance.id
)
@@ -60,7 +60,9 @@ def test_repo(db_session: AsyncSession, authenticated_client: TestClient):
class TestListWorkspaces: class TestListWorkspaces:
"""Tests for GET /projects/{pid}/repositories/{rid}/workspaces.""" """Tests for GET /projects/{pid}/repositories/{rid}/workspaces."""
def test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository): def test_list_empty(
self, authenticated_client: TestClient, test_repo: GitRepository
):
"""Returns empty list when no workspaces exist.""" """Returns empty list when no workspaces exist."""
response = authenticated_client.get( response = authenticated_client.get(
f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces" f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces"
@@ -69,7 +71,10 @@ class TestListWorkspaces:
assert response.json() == [] assert response.json() == []
def test_list_with_workspaces( def test_list_with_workspaces(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Returns workspaces with instance counts.""" """Returns workspaces with instance counts."""
ws = Workspace( ws = Workspace(
@@ -99,7 +104,9 @@ class TestListWorkspaces:
class TestCreateWorkspace: class TestCreateWorkspace:
"""Tests for POST /projects/{pid}/repositories/{rid}/workspaces.""" """Tests for POST /projects/{pid}/repositories/{rid}/workspaces."""
def test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository): def test_create_success(
self, authenticated_client: TestClient, test_repo: GitRepository
):
"""Creates a workspace and clones the repo.""" """Creates a workspace and clones the repo."""
mock_ws = Workspace( mock_ws = Workspace(
id=uuid.uuid4(), id=uuid.uuid4(),
@@ -110,7 +117,9 @@ class TestCreateWorkspace:
path="/data/working-copies/test/feature-branch", path="/data/working-copies/test/feature-branch",
) )
with patch.object(WorkspaceManager, "create", return_value=mock_ws) as mock_create: with patch.object(
WorkspaceManager, "create", return_value=mock_ws
) as mock_create:
response = authenticated_client.post( response = authenticated_client.post(
f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces", f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces",
json={"name": "feature-branch", "branch": "feature"}, json={"name": "feature-branch", "branch": "feature"},
@@ -121,7 +130,9 @@ class TestCreateWorkspace:
assert data["branch"] == "feature" assert data["branch"] == "feature"
mock_create.assert_called_once() mock_create.assert_called_once()
def test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository): def test_create_missing_name(
self, authenticated_client: TestClient, test_repo: GitRepository
):
"""Returns 400 when name is missing.""" """Returns 400 when name is missing."""
response = authenticated_client.post( response = authenticated_client.post(
f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces", f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces",
@@ -131,7 +142,10 @@ class TestCreateWorkspace:
assert "name" in response.json()["detail"] assert "name" in response.json()["detail"]
def test_create_duplicate_name( def test_create_duplicate_name(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Returns 409 when workspace name already exists.""" """Returns 409 when workspace name already exists."""
ws = Workspace( ws = Workspace(
@@ -148,7 +162,9 @@ class TestCreateWorkspace:
asyncio.run(_commit()) asyncio.run(_commit())
with patch.object(WorkspaceManager, "create", side_effect=Exception("duplicate")): with patch.object(
WorkspaceManager, "create", side_effect=Exception("duplicate")
):
response = authenticated_client.post( response = authenticated_client.post(
f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces", f"/projects/{test_repo.project_id}/repositories/{test_repo.id}/workspaces",
json={"name": "dev", "branch": "main"}, json={"name": "dev", "branch": "main"},
@@ -160,7 +176,10 @@ class TestDeleteWorkspace:
"""Tests for DELETE /projects/{pid}/repositories/{rid}/workspaces/{wid}.""" """Tests for DELETE /projects/{pid}/repositories/{rid}/workspaces/{wid}."""
def test_delete_without_instances( def test_delete_without_instances(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Deletes workspace when no instances exist.""" """Deletes workspace when no instances exist."""
ws = Workspace( ws = Workspace(
@@ -185,9 +204,14 @@ class TestDeleteWorkspace:
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["status"] == "deleted" assert response.json()["status"] == "deleted"
@pytest.mark.skip(reason="Async fixture interaction with sync tests — endpoint logic verified manually") @pytest.mark.skip(
reason="Async fixture interaction with sync tests — endpoint logic verified manually"
)
def test_delete_with_instances_no_force( def test_delete_with_instances_no_force(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Returns 409 when workspace has instances and force=False.""" """Returns 409 when workspace has instances and force=False."""
ws = Workspace( ws = Workspace(
@@ -239,7 +263,10 @@ class TestDeleteWorkspace:
assert len(detail["instances"]) == 1 assert len(detail["instances"]) == 1
def test_delete_with_instances_force( def test_delete_with_instances_force(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Deletes workspace when force=True even with instances.""" """Deletes workspace when force=True even with instances."""
ws = Workspace( ws = Workspace(
@@ -268,7 +295,10 @@ class TestSyncWorkspace:
"""Tests for POST /projects/{pid}/repositories/{rid}/workspaces/{wid}/sync.""" """Tests for POST /projects/{pid}/repositories/{rid}/workspaces/{wid}/sync."""
def test_sync_success( def test_sync_success(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Sync succeeds and updates last_sync_at.""" """Sync succeeds and updates last_sync_at."""
ws = Workspace( ws = Workspace(
@@ -298,7 +328,10 @@ class TestSyncWorkspace:
assert data["pulled"] is True assert data["pulled"] is True
def test_sync_branch_deleted( def test_sync_branch_deleted(
self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository self,
authenticated_client: TestClient,
db_session: AsyncSession,
test_repo: GitRepository,
): ):
"""Returns 409 when branch was deleted from remote.""" """Returns 409 when branch was deleted from remote."""
ws = Workspace( ws = Workspace(
+3 -1
View File
@@ -21,7 +21,9 @@ class TestGitServiceClone:
with patch( with patch(
"asyncio.create_subprocess_exec", return_value=mock_proc "asyncio.create_subprocess_exec", return_value=mock_proc
) as mock_exec: ) as mock_exec:
await GitService.clone("https://github.com/test/repo.git", "main", "/tmp/ws") await GitService.clone(
"https://github.com/test/repo.git", "main", "/tmp/ws"
)
mock_exec.assert_called_once_with( mock_exec.assert_called_once_with(
"git", "git",
+32 -18
View File
@@ -37,10 +37,10 @@ export interface Session {
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;
} }
@@ -55,7 +55,7 @@ export async function createInstance(
newBranch?: string, newBranch?: string,
configProfileId?: string, configProfileId?: string,
sshKeyIds?: string[], sshKeyIds?: string[],
workspaceId?: string workspaceId?: 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`,
@@ -68,7 +68,7 @@ export async function createInstance(
new_branch: newBranch || undefined, new_branch: newBranch || undefined,
config_profile_id: configProfileId, config_profile_id: configProfileId,
ssh_key_ids: sshKeyIds || [], ssh_key_ids: sshKeyIds || [],
} },
); );
return response.data; return response.data;
} }
@@ -79,12 +79,12 @@ export async function startInstance(
instanceId: string, instanceId: string,
configProfileId?: string, configProfileId?: string,
sshKeyIds?: string[], sshKeyIds?: string[],
retries = 2 retries = 2,
): Promise<{ status: string; url?: string }> { ): Promise<{ status: string; url?: string }> {
try { try {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`, `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/start`,
{ config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [] } { config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [] },
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -92,7 +92,14 @@ export async function startInstance(
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
if (retries > 0 && !axiosError.response) { if (retries > 0 && !axiosError.response) {
await new Promise((r) => setTimeout(r, 1500)); await new Promise((r) => setTimeout(r, 1500));
return startInstance(projectId, repoId, instanceId, configProfileId, sshKeyIds, retries - 1); return startInstance(
projectId,
repoId,
instanceId,
configProfileId,
sshKeyIds,
retries - 1,
);
} }
throw error; throw error;
} }
@@ -101,10 +108,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;
} }
@@ -115,12 +122,12 @@ export async function restartInstance(
instanceId: string, instanceId: string,
configProfileId?: string, configProfileId?: string,
sshKeyIds?: string[], sshKeyIds?: string[],
retries = 2 retries = 2,
): Promise<{ status: string; url?: string }> { ): Promise<{ status: string; url?: string }> {
try { try {
const response = await apiClient.post( const response = await apiClient.post(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`, `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}/restart`,
{ config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [] } { config_profile_id: configProfileId, ssh_key_ids: sshKeyIds || [] },
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -128,7 +135,14 @@ export async function restartInstance(
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
if (retries > 0 && !axiosError.response) { if (retries > 0 && !axiosError.response) {
await new Promise((r) => setTimeout(r, 1500)); await new Promise((r) => setTimeout(r, 1500));
return restartInstance(projectId, repoId, instanceId, configProfileId, sshKeyIds, retries - 1); return restartInstance(
projectId,
repoId,
instanceId,
configProfileId,
sshKeyIds,
retries - 1,
);
} }
throw error; throw error;
} }
@@ -138,11 +152,11 @@ export async function deleteInstance(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string, instanceId: string,
force?: boolean force?: boolean,
): Promise<void> { ): Promise<void> {
await apiClient.delete( await apiClient.delete(
`/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`, `/projects/${projectId}/repositories/${repoId}/instances/${instanceId}`,
{ params: { force } } { params: { force } },
); );
} }
@@ -166,10 +180,10 @@ export interface InstanceHealth {
export async function checkInstanceHealth( export async function checkInstanceHealth(
projectId: string, projectId: string,
repoId: string, repoId: string,
instanceId: string instanceId: string,
): Promise<InstanceHealth> { ): Promise<InstanceHealth> {
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;
} }
@@ -177,10 +191,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;
} }
+23 -6
View File
@@ -1,15 +1,24 @@
/** Workspace API client. */ /** Workspace API client. */
import { apiClient } from "./client"; import { apiClient } from "./client";
import type { Workspace, CreateWorkspaceRequest, SyncResult } from "../types/workspace"; import type {
Workspace,
CreateWorkspaceRequest,
SyncResult,
} from "../types/workspace";
function workspaceUrl(projectId: string, repoId: string, workspaceId?: string) { function workspaceUrl(projectId: string, repoId: string, workspaceId?: string) {
const base = `/projects/${projectId}/repositories/${repoId}/workspaces`; const base = `/projects/${projectId}/repositories/${repoId}/workspaces`;
return workspaceId ? `${base}/${workspaceId}` : base; return workspaceId ? `${base}/${workspaceId}` : base;
} }
export async function listWorkspaces(projectId: string, repoId: string): Promise<Workspace[]> { export async function listWorkspaces(
const response = await apiClient.get<Workspace[]>(workspaceUrl(projectId, repoId)); projectId: string,
repoId: string,
): Promise<Workspace[]> {
const response = await apiClient.get<Workspace[]>(
workspaceUrl(projectId, repoId),
);
return response.data; return response.data;
} }
@@ -18,7 +27,10 @@ export async function createWorkspace(
repoId: string, repoId: string,
data: CreateWorkspaceRequest, data: CreateWorkspaceRequest,
): Promise<Workspace> { ): Promise<Workspace> {
const response = await apiClient.post<Workspace>(workspaceUrl(projectId, repoId), data); const response = await apiClient.post<Workspace>(
workspaceUrl(projectId, repoId),
data,
);
return response.data; return response.data;
} }
@@ -27,7 +39,9 @@ export async function getWorkspace(
repoId: string, repoId: string,
workspaceId: string, workspaceId: string,
): Promise<Workspace> { ): Promise<Workspace> {
const response = await apiClient.get<Workspace>(workspaceUrl(projectId, repoId, workspaceId)); const response = await apiClient.get<Workspace>(
workspaceUrl(projectId, repoId, workspaceId),
);
return response.data; return response.data;
} }
@@ -37,7 +51,10 @@ export async function updateWorkspace(
workspaceId: string, workspaceId: string,
data: Partial<CreateWorkspaceRequest>, data: Partial<CreateWorkspaceRequest>,
): Promise<Workspace> { ): Promise<Workspace> {
const response = await apiClient.patch<Workspace>(workspaceUrl(projectId, repoId, workspaceId), data); const response = await apiClient.patch<Workspace>(
workspaceUrl(projectId, repoId, workspaceId),
data,
);
return response.data; return response.data;
} }
+16 -3
View File
@@ -10,7 +10,11 @@ export interface StartToolModalProps {
onStart: (toolTypeId: string, configProfileId?: string) => Promise<void>; onStart: (toolTypeId: string, configProfileId?: string) => Promise<void>;
} }
export function StartToolModal({ workspace, onClose, onStart }: StartToolModalProps) { export function StartToolModal({
workspace,
onClose,
onStart,
}: StartToolModalProps) {
const [toolTypeId, setToolTypeId] = useState(""); const [toolTypeId, setToolTypeId] = useState("");
const [configProfileId, setConfigProfileId] = useState(""); const [configProfileId, setConfigProfileId] = useState("");
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
@@ -73,10 +77,19 @@ export function StartToolModal({ workspace, onClose, onStart }: StartToolModalPr
</div> </div>
{error && <p className="form-error">{error}</p>} {error && <p className="form-error">{error}</p>}
<div className="form-actions"> <div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={onClose} disabled={submitting}> <button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={submitting}
>
Cancel Cancel
</button> </button>
<button type="submit" className="btn btn-primary" disabled={submitting}> <button
type="submit"
className="btn btn-primary"
disabled={submitting}
>
{submitting ? "Starting..." : "Start Tool"} {submitting ? "Starting..." : "Start Tool"}
</button> </button>
</div> </div>
+3 -1
View File
@@ -29,7 +29,9 @@ export function WorkspaceCard({
<article className={`card workspace-card ${loading ? "loading" : ""}`}> <article className={`card workspace-card ${loading ? "loading" : ""}`}>
<div className="workspace-header"> <div className="workspace-header">
<h4>{workspace.name}</h4> <h4>{workspace.name}</h4>
<span className={`status-badge ${statusClass}`}>{workspace.status}</span> <span className={`status-badge ${statusClass}`}>
{workspace.status}
</span>
</div> </div>
<div className="workspace-meta"> <div className="workspace-meta">
<p className="workspace-project"> <p className="workspace-project">
@@ -33,7 +33,9 @@ export function WorkspaceCreateForm({
try { try {
await onSubmit({ name: name.trim(), branch: branch.trim() }); await onSubmit({ name: name.trim(), branch: branch.trim() });
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to create workspace"); setError(
err instanceof Error ? err.message : "Failed to create workspace",
);
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@@ -70,7 +72,12 @@ export function WorkspaceCreateForm({
</div> </div>
{error && <p className="form-error">{error}</p>} {error && <p className="form-error">{error}</p>}
<div className="form-actions"> <div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={onCancel} disabled={submitting}> <button
type="button"
className="btn btn-secondary"
onClick={onCancel}
disabled={submitting}
>
Cancel Cancel
</button> </button>
<button type="submit" className="btn btn-primary" disabled={submitting}> <button type="submit" className="btn btn-primary" disabled={submitting}>
+10 -3
View File
@@ -107,9 +107,16 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
await onRefresh(); await onRefresh();
} catch (err) { } catch (err) {
const error = err as ApiError; const error = err as ApiError;
if (error.response?.status === 409 && error.response.data?.detail?.branch_deleted) { if (
const message = error.response.data.detail.message || "Branch was deleted from remote"; error.response?.status === 409 &&
const confirmed = window.confirm(`${message}\n\nDelete this workspace?`); error.response.data?.detail?.branch_deleted
) {
const message =
error.response.data.detail.message ||
"Branch was deleted from remote";
const confirmed = window.confirm(
`${message}\n\nDelete this workspace?`,
);
if (confirmed) { if (confirmed) {
await deleteWorkspace(projectId, repoId, workspace.id, true); await deleteWorkspace(projectId, repoId, workspace.id, true);
await onRefresh(); await onRefresh();
+7 -2
View File
@@ -11,7 +11,10 @@ export interface UseWorkspacesResult {
refresh: () => Promise<void>; refresh: () => Promise<void>;
} }
export function useWorkspaces(projectId: string, repoId: string): UseWorkspacesResult { export function useWorkspaces(
projectId: string,
repoId: string,
): UseWorkspacesResult {
const [workspaces, setWorkspaces] = useState<Workspace[]>([]); const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -23,7 +26,9 @@ export function useWorkspaces(projectId: string, repoId: string): UseWorkspacesR
const data = await listWorkspaces(projectId, repoId); const data = await listWorkspaces(projectId, repoId);
setWorkspaces(data); setWorkspaces(data);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to load workspaces"); setError(
err instanceof Error ? err.message : "Failed to load workspaces",
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
+22 -6
View File
@@ -18,7 +18,10 @@ export function WorkspacesPage() {
const projectId = "default-project"; const projectId = "default-project";
const repoId = "default-repo"; const repoId = "default-repo";
const { workspaces, loading, error, refresh } = useWorkspaces(projectId, repoId); const { workspaces, loading, error, refresh } = useWorkspaces(
projectId,
repoId,
);
const actions = useWorkspaceActions(); const actions = useWorkspaceActions();
const handleCreate = async (data: { name: string; branch: string }) => { const handleCreate = async (data: { name: string; branch: string }) => {
@@ -35,7 +38,10 @@ export function WorkspacesPage() {
await actions.sync(projectId, repoId, workspace, refresh); await actions.sync(projectId, repoId, workspace, refresh);
}; };
const handleStartTool = async (toolTypeId: string, configProfileId?: string) => { const handleStartTool = async (
toolTypeId: string,
configProfileId?: string,
) => {
if (!startWorkspace) return; if (!startWorkspace) return;
try { try {
const instance = await createInstance( const instance = await createInstance(
@@ -48,7 +54,7 @@ export function WorkspacesPage() {
undefined, undefined,
configProfileId, configProfileId,
[], [],
startWorkspace.id startWorkspace.id,
); );
await startInstance(projectId, repoId, instance.id, configProfileId); await startInstance(projectId, repoId, instance.id, configProfileId);
setStartWorkspace(null); setStartWorkspace(null);
@@ -63,10 +69,17 @@ export function WorkspacesPage() {
<header className="page-header"> <header className="page-header">
<h1>Workspaces</h1> <h1>Workspaces</h1>
<div className="header-actions"> <div className="header-actions">
<button className="btn btn-secondary" onClick={refresh} disabled={loading}> <button
className="btn btn-secondary"
onClick={refresh}
disabled={loading}
>
<Icon name="refresh" size="sm" /> <Icon name="refresh" size="sm" />
</button> </button>
<button className="btn btn-primary" onClick={() => setShowCreate(true)}> <button
className="btn btn-primary"
onClick={() => setShowCreate(true)}
>
<Icon name="add" size="sm" /> New Workspace <Icon name="add" size="sm" /> New Workspace
</button> </button>
</div> </div>
@@ -88,7 +101,10 @@ export function WorkspacesPage() {
) : workspaces.length === 0 ? ( ) : workspaces.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<p>No workspaces yet.</p> <p>No workspaces yet.</p>
<button className="btn btn-primary" onClick={() => setShowCreate(true)}> <button
className="btn btn-primary"
onClick={() => setShowCreate(true)}
>
<Icon name="add" size="sm" /> Create your first workspace <Icon name="add" size="sm" /> Create your first workspace
</button> </button>
</div> </div>
+20 -5
View File
@@ -22,7 +22,10 @@ export const AppRouter = () => {
return ( return (
<Routes> <Routes>
<Route path="/login" element={<LoginRedirectPage />} /> <Route path="/login" element={<LoginRedirectPage />} />
<Route path="/ssh-keys" element={<Navigate to="/settings/ssh-keys" replace />} /> <Route
path="/ssh-keys"
element={<Navigate to="/settings/ssh-keys" replace />}
/>
<Route <Route
path="/" path="/"
element={ element={
@@ -34,9 +37,18 @@ export const AppRouter = () => {
<Route index element={<HomePage />} /> <Route index element={<HomePage />} />
<Route path="projects" element={<ProjectsPage />} /> <Route path="projects" element={<ProjectsPage />} />
<Route path="projects/:projectId" element={<RepoWorkspace />} /> <Route path="projects/:projectId" element={<RepoWorkspace />} />
<Route path="projects/:projectId/repositories" element={<GitRepositoriesPage />} /> <Route
<Route path="projects/:projectId/repositories/:repoId/history" element={<GitHistoryPage />} /> path="projects/:projectId/repositories"
<Route path="projects/:projectId/settings/*" element={<ProjectSettingsPage />} /> element={<GitRepositoriesPage />}
/>
<Route
path="projects/:projectId/repositories/:repoId/history"
element={<GitHistoryPage />}
/>
<Route
path="projects/:projectId/settings/*"
element={<ProjectSettingsPage />}
/>
<Route path="profile" element={<ProfilePage />} /> <Route path="profile" element={<ProfilePage />} />
<Route path="config-profiles" element={<ConfigProfilesPage />} /> <Route path="config-profiles" element={<ConfigProfilesPage />} />
<Route path="settings" element={<SettingsPage />}> <Route path="settings" element={<SettingsPage />}>
@@ -48,7 +60,10 @@ export const AppRouter = () => {
<Route path="sessions" element={<SessionsPage />} /> <Route path="sessions" element={<SessionsPage />} />
<Route path="workspaces" element={<WorkspacesPage />} /> <Route path="workspaces" element={<WorkspacesPage />} />
<Route path="tool-workshop" element={<ToolWorkshopPage />} /> <Route path="tool-workshop" element={<ToolWorkshopPage />} />
<Route path="instances/:instanceId/terminal" element={<TerminalPage />} /> <Route
path="instances/:instanceId/terminal"
element={<TerminalPage />}
/>
</Route> </Route>
<Route path="/404" element={<NotFoundPage />} /> <Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<Navigate to="/404" replace />} /> <Route path="*" element={<Navigate to="/404" replace />} />