feat: busy container overlays for lifecycle actions

Add a reusable LoadingOverlay component that dims and disables the
container owning an in-flight action, with a spinning indicator and
label. Apply it to:

- SessionCard (when actionBusyId matches)
- InstanceList cards (per busyInstanceId with action-specific labels)
- CreateSessionForm (while submitting)
- ToolStarter (while starting)

Also add .icon-spin animation and position:relative to the relevant
containers.

Quality gates: npm run typecheck, npm run lint, npm test -- --run
(87 passed).
This commit is contained in:
Developer
2026-06-13 10:10:28 +00:00
parent 8a8a9bc9e4
commit f8b162ec5f
6 changed files with 84 additions and 0 deletions
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Icon } from "../../icon";
import { LoadingOverlay } from "../../loading-overlay";
import type { ToolInstance } from "../../../api/sessions";
import {
deleteInstance,
@@ -60,6 +61,7 @@ export const InstanceList = ({
// Per-instance busy state for actions
const [busyInstanceId, setBusyInstanceId] = useState<string | null>(null);
const [busyLabel, setBusyLabel] = useState("Working...");
const loadInstances = useCallback(async () => {
setLoading(true);
@@ -135,6 +137,7 @@ export const InstanceList = ({
sshKeyIds?: string[],
) => {
setBusyInstanceId(instanceId);
setBusyLabel("Starting...");
try {
await startInstance(
projectId,
@@ -156,6 +159,7 @@ export const InstanceList = ({
const handleStop = async (instanceId: string) => {
setBusyInstanceId(instanceId);
setBusyLabel("Stopping...");
try {
await stopInstance(projectId, repoId, instanceId);
setStopConfirmId(null);
@@ -173,6 +177,7 @@ export const InstanceList = ({
sshKeyIds?: string[],
) => {
setBusyInstanceId(instanceId);
setBusyLabel("Restarting...");
try {
await restartInstance(
projectId,
@@ -195,6 +200,7 @@ export const InstanceList = ({
const handleDelete = async (instanceId: string) => {
if (!confirm("Are you sure you want to delete this instance?")) return;
setBusyInstanceId(instanceId);
setBusyLabel("Deleting...");
try {
await deleteInstance(projectId, repoId, instanceId);
// Update state immediately instead of reloading
@@ -250,6 +256,10 @@ export const InstanceList = ({
<div className="instance-grid">
{instances.map((instance) => (
<div key={instance.id} className="instance-card">
<LoadingOverlay
visible={busyInstanceId === instance.id}
label={busyLabel}
/>
<div className="instance-info">
<div className="instance-name">{instance.display_name}</div>
<div className="instance-meta">