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
+133 -126
View File
@@ -2,145 +2,152 @@
import { useState, useCallback } from "react";
import {
createWorkspace,
deleteWorkspace,
syncWorkspace,
updateWorkspace,
createWorkspace,
deleteWorkspace,
syncWorkspace,
updateWorkspace,
} from "../api/workspaces";
import type { Workspace, CreateWorkspaceRequest } from "../types/workspace";
export interface UseWorkspaceActionsResult {
loadingId: string | null;
create: (
projectId: string,
repoId: string,
data: CreateWorkspaceRequest,
) => Promise<Workspace>;
delete: (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => Promise<void>;
sync: (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => Promise<void>;
update: (
projectId: string,
repoId: string,
workspaceId: string,
data: Partial<CreateWorkspaceRequest>,
) => Promise<Workspace>;
loadingId: string | null;
create: (
projectId: string,
repoId: string,
data: CreateWorkspaceRequest,
) => Promise<Workspace>;
delete: (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => Promise<void>;
sync: (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => Promise<void>;
update: (
projectId: string,
repoId: string,
workspaceId: string,
data: Partial<CreateWorkspaceRequest>,
) => Promise<Workspace>;
}
interface ApiError {
response?: {
status?: number;
data?: {
detail?: {
message?: string;
instances?: Array<{ id: string; name: string }>;
branch_deleted?: boolean;
};
};
};
response?: {
status?: number;
data?: {
detail?: {
message?: string;
instances?: Array<{ id: string; name: string }>;
branch_deleted?: boolean;
};
};
};
}
export function useWorkspaceActions(): UseWorkspaceActionsResult {
const [loadingId, setLoadingId] = useState<string | null>(null);
const [loadingId, setLoadingId] = useState<string | null>(null);
const create = useCallback(
async (projectId: string, repoId: string, data: CreateWorkspaceRequest) => {
return createWorkspace(projectId, repoId, data);
},
[],
);
const create = useCallback(
async (projectId: string, repoId: string, data: CreateWorkspaceRequest) => {
return createWorkspace(projectId, repoId, data);
},
[],
);
const deleteAction = useCallback(
async (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => {
setLoadingId(workspace.id);
try {
await deleteWorkspace(projectId, repoId, workspace.id);
await onRefresh();
} catch (err) {
const error = err as ApiError;
if (error.response?.status === 409) {
const detail = error.response.data?.detail;
const instances = detail?.instances || [];
const confirmed = window.confirm(
`This workspace has ${instances.length} running tool instance(s):\n` +
instances.map((i) => `- ${i.name}`).join("\n") +
`\n\nDelete workspace and all instances?`,
);
if (confirmed) {
await deleteWorkspace(projectId, repoId, workspace.id, true);
await onRefresh();
}
} else {
throw err;
}
} finally {
setLoadingId(null);
}
},
[],
);
const deleteAction = useCallback(
async (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => {
setLoadingId(workspace.id);
try {
await deleteWorkspace(projectId, repoId, workspace.id);
await onRefresh();
} catch (err) {
const error = err as ApiError;
if (error.response?.status === 409) {
const detail = error.response.data?.detail;
const instances = detail?.instances || [];
const confirmed = window.confirm(
`This workspace has ${instances.length} running tool instance(s):\n` +
instances.map((i) => `- ${i.name}`).join("\n") +
`\n\nDelete workspace and all instances?`,
);
if (confirmed) {
await deleteWorkspace(projectId, repoId, workspace.id, true);
await onRefresh();
}
} else {
throw err;
}
} finally {
setLoadingId(null);
}
},
[],
);
const sync = useCallback(
async (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => {
setLoadingId(workspace.id);
try {
await syncWorkspace(projectId, repoId, workspace.id);
await onRefresh();
} catch (err) {
const error = err as ApiError;
if (error.response?.status === 409 && 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) {
await deleteWorkspace(projectId, repoId, workspace.id, true);
await onRefresh();
}
} else {
throw err;
}
} finally {
setLoadingId(null);
}
},
[],
);
const sync = useCallback(
async (
projectId: string,
repoId: string,
workspace: Workspace,
onRefresh: () => Promise<void>,
) => {
setLoadingId(workspace.id);
try {
await syncWorkspace(projectId, repoId, workspace.id);
await onRefresh();
} catch (err) {
const error = err as ApiError;
if (
error.response?.status === 409 &&
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) {
await deleteWorkspace(projectId, repoId, workspace.id, true);
await onRefresh();
}
} else {
throw err;
}
} finally {
setLoadingId(null);
}
},
[],
);
const update = useCallback(
async (
projectId: string,
repoId: string,
workspaceId: string,
data: Partial<CreateWorkspaceRequest>,
) => {
return updateWorkspace(projectId, repoId, workspaceId, data);
},
[],
);
const update = useCallback(
async (
projectId: string,
repoId: string,
workspaceId: string,
data: Partial<CreateWorkspaceRequest>,
) => {
return updateWorkspace(projectId, repoId, workspaceId, data);
},
[],
);
return {
loadingId,
create,
delete: deleteAction,
sync,
update,
};
return {
loadingId,
create,
delete: deleteAction,
sync,
update,
};
}
+29 -24
View File
@@ -5,33 +5,38 @@ import { listWorkspaces } from "../api/workspaces";
import type { Workspace } from "../types/workspace";
export interface UseWorkspacesResult {
workspaces: Workspace[];
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
workspaces: Workspace[];
loading: boolean;
error: string | null;
refresh: () => Promise<void>;
}
export function useWorkspaces(projectId: string, repoId: string): UseWorkspacesResult {
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
export function useWorkspaces(
projectId: string,
repoId: string,
): UseWorkspacesResult {
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await listWorkspaces(projectId, repoId);
setWorkspaces(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load workspaces");
} finally {
setLoading(false);
}
}, [projectId, repoId]);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await listWorkspaces(projectId, repoId);
setWorkspaces(data);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load workspaces",
);
} finally {
setLoading(false);
}
}, [projectId, repoId]);
useEffect(() => {
refresh();
}, [refresh]);
useEffect(() => {
refresh();
}, [refresh]);
return { workspaces, loading, error, refresh };
return { workspaces, loading, error, refresh };
}