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).
This commit is contained in:
Developer
2026-06-12 13:19:58 +00:00
parent 110844e597
commit 7440720b7b
30 changed files with 1664 additions and 845 deletions
@@ -17,6 +17,7 @@ import {
} from "../../../api/config-profiles";
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
import { useEventContext } from "../../../state/events";
import { useSessions } from "../../../state/sessions";
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8000";
@@ -37,6 +38,7 @@ export const InstanceList = ({
toolTypes,
}: InstanceListProps) => {
const navigate = useNavigate();
const { refreshSessions } = useSessions();
const [instances, setInstances] = useState<ToolInstance[]>([]);
const [loading, setLoading] = useState(false);
const [showCreate, setShowCreate] = useState(false);
@@ -108,6 +110,7 @@ export const InstanceList = ({
const handleCreateSuccess = async () => {
setShowCreate(false);
await loadInstances();
await refreshSessions();
};
const loadConfigProfiles = useCallback(
@@ -196,6 +199,7 @@ export const InstanceList = ({
await deleteInstance(projectId, repoId, instanceId);
// Update state immediately instead of reloading
setInstances((prev) => prev.filter((i) => i.id !== instanceId));
await refreshSessions();
} catch {
setError("Failed to delete instance");
} finally {
@@ -245,15 +249,7 @@ export const InstanceList = ({
) : (
<div className="instance-grid">
{instances.map((instance) => (
<div
key={instance.id}
className={`instance-card ${busyInstanceId === instance.id ? "busy" : ""}`}
>
{busyInstanceId === instance.id && (
<div className="instance-busy-overlay">
<Icon name="loading" size="md" />
</div>
)}
<div key={instance.id} className="instance-card">
<div className="instance-info">
<div className="instance-name">{instance.display_name}</div>
<div className="instance-meta">
@@ -8,6 +8,8 @@ import {
type ConfigProfile,
} from "../../../api/config-profiles";
import { listSSHKeys, type SSHKey } from "../../../api/ssh-keys";
import { useSessions } from "../../../state/sessions";
import { useSessionOperations } from "../../../state/session-operations";
import type { Workspace } from "../../../types/workspace";
import type { ToolInstance } from "../../../api/sessions";
@@ -22,6 +24,8 @@ export function ToolStarter({
onStarted,
onCancel,
}: ToolStarterProps) {
const { addOrUpdateSession } = useSessions();
const { startOperation } = useSessionOperations();
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [toolTypesLoading, setToolTypesLoading] = useState(true);
const [toolTypesError, setToolTypesError] = useState<string | null>(null);
@@ -141,6 +145,21 @@ export function ToolStarter({
selectedProfileId || undefined,
selectedSshKeyIds.length > 0 ? selectedSshKeyIds : undefined,
);
addOrUpdateSession({
id: instance.id,
display_name: instance.display_name,
tool_type_name: instance.tool_type_name,
tool_icon: "code",
tool_type_interfaces: instance.tool_type_interfaces || [],
repository_name: workspace.repo_name,
repository_id: workspace.repo_id,
project_name: workspace.project_name,
project_id: workspace.project_id,
workspace_name: workspace.name,
status: instance.status || "pending",
url: instance.url || null,
});
startOperation("create", instance.id, instance.display_name);
onStarted(instance);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to start tool");
@@ -153,6 +172,9 @@ export function ToolStarter({
displayName,
workspace,
onStarted,
toolTypes,
addOrUpdateSession,
startOperation,
]);
return (