Files
headquarter/apps/web/src/components/features/session/session-progress-panel.tsx
T
Developer 7440720b7b feat: implement tool-session progress panel and live list updates
- Add SessionOperationsContext + SessionProgressPanel for global,
  non-blocking lifecycle progress (create/start/stop/restart/delete/
  recreate-tunnel) driven by SSE events.
- Promote SessionsContext to authoritative shared session state with
  refresh, addOrUpdateSession, and removeSession helpers.
- Wire AppShell, DashboardPage, SessionsPage, useInstanceActions,
  ToolStarter, and InstanceList into shared state so lists update
  immediately after create/delete without manual refresh.
- Remove legacy blocking overlays from CreateSessionForm, SessionCard,
  and InstanceList; keep disabled states and inline spinners only.
- Update DashboardPage tests to wrap with SessionsProvider and
  SessionOperationsProvider.
- Add .cache/ to .gitignore.

Quality gates: npm run typecheck, npm run lint, npm test -- --run
(82 passed).
2026-06-12 13:19:58 +00:00

116 lines
3.2 KiB
TypeScript

import { useEffect } from "react";
import {
useSessionOperations,
type Operation,
} from "../../../state/session-operations";
import { useEventContext } from "../../../state/events";
import { Icon } from "../../icon";
interface StepConfig {
label: string;
index: number;
}
const steps: StepConfig[] = [
{ label: "Created", index: 1 },
{ label: "Building", index: 2 },
{ label: "Starting", index: 3 },
{ label: "Ready", index: 4 },
];
function OperationItem({
operation,
onDismiss,
}: {
operation: Operation;
onDismiss: () => void;
}) {
const isDone = operation.status === "success" || operation.status === "error";
const isError = operation.status === "error";
return (
<div
className={`session-operation-item ${operation.status}`}
role="status"
aria-live="polite"
>
<div className="session-operation-header">
<div className="session-operation-title">
{isError ? (
<Icon name="error" size="sm" />
) : isDone ? (
<Icon name="success" size="sm" />
) : (
<Icon name="loading" size="sm" />
)}
<span className="session-operation-name">{operation.displayName}</span>
</div>
{isDone && (
<button
type="button"
className="session-operation-dismiss"
onClick={onDismiss}
aria-label="Dismiss"
>
<Icon name="close" size="sm" />
</button>
)}
</div>
<p className="session-operation-message">{operation.message}</p>
<div className="session-operation-steps">
{steps.map((step) => {
const active = operation.step >= step.index;
const current = operation.step === step.index && !isDone;
return (
<span
key={step.label}
className={`session-operation-step ${active ? "active" : ""} ${current ? "current" : ""}`}
>
{step.label}
</span>
);
})}
</div>
</div>
);
}
export function SessionProgressPanel() {
const { operations, updateOperationFromEvent, dismissOperation } =
useSessionOperations();
const { events } = useEventContext();
useEffect(() => {
if (events.length === 0) return;
const latestEvent = events[events.length - 1];
updateOperationFromEvent(latestEvent);
}, [events, updateOperationFromEvent]);
const visibleOperations = operations.filter(
(op) =>
op.status === "pending" ||
op.status === "active" ||
(op.status === "success" && Date.now() - op.createdAt < 5000) ||
op.status === "error",
);
if (visibleOperations.length === 0) return null;
return (
<div className="session-progress-panel" role="region" aria-label="Session operations">
<div className="session-progress-panel-header">
<span className="session-progress-panel-title">Operations</span>
</div>
<div className="session-progress-panel-list">
{visibleOperations.map((operation) => (
<OperationItem
key={operation.id}
operation={operation}
onDismiss={() => dismissOperation(operation.id)}
/>
))}
</div>
</div>
);
}