51a399c775
Sidebar SessionItem was only opening web tool URLs in new tabs. Terminal sessions linked to the project page in the same tab. SessionCard 'Open' buttons for terminal sessions navigated in-place. Changes: - app-shell.tsx: SessionItem now builds terminal URLs (/instances/:id/terminal) and always uses target=_blank - session-card.tsx: compute openHref for both web and terminal sessions, render <a> links with target=_blank instead of callback buttons - use-instance-actions.ts: handleOpen now uses window.open(..., '_blank') for terminal sessions and project fallback All session opening (sidebar, cards, callbacks) now consistently opens in a new tab. Quality gates: tsc clean
188 lines
4.5 KiB
TypeScript
188 lines
4.5 KiB
TypeScript
import { useState, useCallback } from "react";
|
|
import {
|
|
stopInstance,
|
|
deleteInstance,
|
|
startInstance,
|
|
recreateInstanceTunnel,
|
|
} from "../api/sessions";
|
|
import type { Session } from "../api/sessions";
|
|
|
|
interface UseInstanceActionsOptions {
|
|
onRefresh: () => Promise<void>;
|
|
}
|
|
|
|
interface UseInstanceActionsReturn {
|
|
loadingSessionId: string | null;
|
|
dirtyDeleteSession: Session | null;
|
|
dirtyDeleteFiles: string[];
|
|
handleOpen: (session: Session) => void;
|
|
handleStart: (session: Session) => Promise<void>;
|
|
handleStop: (session: Session) => Promise<void>;
|
|
handleDelete: (session: Session) => Promise<void>;
|
|
handleForceDelete: (session: Session) => Promise<void>;
|
|
handleRecreateTunnel: (session: Session) => Promise<void>;
|
|
clearDirtyDelete: () => void;
|
|
}
|
|
|
|
export function useInstanceActions(
|
|
options: UseInstanceActionsOptions,
|
|
): UseInstanceActionsReturn {
|
|
const { onRefresh } = options;
|
|
const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
|
|
const [dirtyDeleteSession, setDirtyDeleteSession] = useState<Session | null>(
|
|
null,
|
|
);
|
|
const [dirtyDeleteFiles, setDirtyDeleteFiles] = useState<string[]>([]);
|
|
|
|
const handleOpen = useCallback((session: Session) => {
|
|
if (session.url) {
|
|
window.open(session.url, "_blank", "noopener,noreferrer");
|
|
return;
|
|
}
|
|
if (session.tool_type_interfaces?.includes("terminal")) {
|
|
window.open(`/instances/${session.id}/terminal`, "_blank", "noopener,noreferrer");
|
|
return;
|
|
}
|
|
window.open(`/projects/${session.project_id}`, "_blank", "noopener,noreferrer");
|
|
}, []);
|
|
|
|
const handleStart = useCallback(
|
|
async (session: Session) => {
|
|
if (loadingSessionId === session.id) return;
|
|
setLoadingSessionId(session.id);
|
|
try {
|
|
await startInstance(
|
|
session.project_id,
|
|
session.repository_id,
|
|
session.id,
|
|
);
|
|
await onRefresh();
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoadingSessionId(null);
|
|
}
|
|
},
|
|
[loadingSessionId, onRefresh],
|
|
);
|
|
|
|
const handleStop = useCallback(
|
|
async (session: Session) => {
|
|
if (loadingSessionId === session.id) return;
|
|
setLoadingSessionId(session.id);
|
|
try {
|
|
await stopInstance(
|
|
session.project_id,
|
|
session.repository_id,
|
|
session.id,
|
|
);
|
|
await onRefresh();
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoadingSessionId(null);
|
|
}
|
|
},
|
|
[loadingSessionId, onRefresh],
|
|
);
|
|
|
|
const handleDelete = useCallback(
|
|
async (session: Session) => {
|
|
if (loadingSessionId === session.id) return;
|
|
setLoadingSessionId(session.id);
|
|
try {
|
|
await deleteInstance(
|
|
session.project_id,
|
|
session.repository_id,
|
|
session.id,
|
|
);
|
|
setDirtyDeleteSession(null);
|
|
setDirtyDeleteFiles([]);
|
|
await onRefresh();
|
|
} catch (error) {
|
|
const axiosError = error as {
|
|
response?: {
|
|
status?: number;
|
|
data?: { detail?: { changed_files?: string[] } };
|
|
};
|
|
};
|
|
if (axiosError.response?.status === 409) {
|
|
const detail = axiosError.response.data?.detail;
|
|
if (detail?.changed_files) {
|
|
setDirtyDeleteSession(session);
|
|
setDirtyDeleteFiles(detail.changed_files);
|
|
return;
|
|
}
|
|
}
|
|
} finally {
|
|
setLoadingSessionId(null);
|
|
}
|
|
},
|
|
[loadingSessionId, onRefresh],
|
|
);
|
|
|
|
const handleForceDelete = useCallback(
|
|
async (session: Session) => {
|
|
if (loadingSessionId === session.id) return;
|
|
setLoadingSessionId(session.id);
|
|
try {
|
|
await deleteInstance(
|
|
session.project_id,
|
|
session.repository_id,
|
|
session.id,
|
|
true,
|
|
);
|
|
setDirtyDeleteSession(null);
|
|
setDirtyDeleteFiles([]);
|
|
await onRefresh();
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setLoadingSessionId(null);
|
|
}
|
|
},
|
|
[loadingSessionId, onRefresh],
|
|
);
|
|
|
|
const handleRecreateTunnel = useCallback(
|
|
async (session: Session) => {
|
|
if (loadingSessionId === session.id) return;
|
|
setLoadingSessionId(session.id);
|
|
try {
|
|
await recreateInstanceTunnel(
|
|
session.project_id,
|
|
session.repository_id,
|
|
session.id,
|
|
);
|
|
await onRefresh();
|
|
} catch (err) {
|
|
const message =
|
|
(err as { response?: { data?: { detail?: string } } })?.response?.data
|
|
?.detail || "Failed to recreate tunnel";
|
|
alert(message);
|
|
} finally {
|
|
setLoadingSessionId(null);
|
|
}
|
|
},
|
|
[loadingSessionId, onRefresh],
|
|
);
|
|
|
|
const clearDirtyDelete = useCallback(() => {
|
|
setDirtyDeleteSession(null);
|
|
setDirtyDeleteFiles([]);
|
|
}, []);
|
|
|
|
return {
|
|
loadingSessionId,
|
|
dirtyDeleteSession,
|
|
dirtyDeleteFiles,
|
|
handleOpen,
|
|
handleStart,
|
|
handleStop,
|
|
handleDelete,
|
|
handleForceDelete,
|
|
handleRecreateTunnel,
|
|
clearDirtyDelete,
|
|
};
|
|
}
|