feat(web): unify panel loading states with grid spinner (#1142)

This commit is contained in:
Carlos Florêncio
2026-05-29 11:08:42 +01:00
committed by GitHub
parent 1fcc581a09
commit cf2cd1bd2f
4 changed files with 41 additions and 33 deletions
@@ -0,0 +1,30 @@
"use client";
import { GridSpinner } from "@/components/grid-spinner";
import { cn } from "@/lib/utils";
type PanelLoadingStateProps = {
label: string;
/** Override the outer container layout (e.g. absolute overlay for the terminal). */
className?: string;
testId?: string;
};
/**
* Shared panel loading placeholder: a grid spinner above a label, matching the
* terminal's "Connecting terminal..." look. Used by the file browser, terminal,
* and agent preview panels so loading states stay visually consistent.
*/
export function PanelLoadingState({ label, className, testId }: PanelLoadingStateProps) {
return (
<div
data-testid={testId}
className={cn("flex h-full w-full items-start justify-center pt-12", className)}
>
<div className="flex flex-col items-center gap-3 text-muted-foreground">
<GridSpinner />
<span className="text-sm">{label}</span>
</div>
</div>
);
}
@@ -1,6 +1,6 @@
"use client";
import { IconLoader2 } from "@tabler/icons-react";
import { PanelLoadingState } from "@/components/panel-loading-state";
import type { FileTreeNode } from "@/lib/types/backend";
type RenderSessionOrLoadStateInput = {
@@ -31,18 +31,10 @@ export function renderSessionOrLoadState({
);
}
if ((loadState === "loading" || isLoadingTree) && !tree) {
return <div className="p-4 text-sm text-muted-foreground">Loading files...</div>;
return <PanelLoadingState label="Loading files..." />;
}
if (loadState === "waiting") {
return (
<div
data-testid="file-tree-waiting"
className="p-4 text-sm text-muted-foreground flex items-center gap-2"
>
<IconLoader2 className="h-3.5 w-3.5 animate-spin" />
Preparing workspace...
</div>
);
return <PanelLoadingState testId="file-tree-waiting" label="Preparing workspace..." />;
}
if (loadState === "manual") {
return (
@@ -6,7 +6,7 @@ import { FitAddon } from "@xterm/addon-fit";
import { AttachAddon } from "@xterm/addon-attach";
import { WebglAddon } from "@xterm/addon-webgl";
import "@xterm/xterm/css/xterm.css";
import { GridSpinner } from "@/components/grid-spinner";
import { PanelLoadingState } from "@/components/panel-loading-state";
import { useAppStore } from "@/components/state-provider";
import { useSession } from "@/hooks/domains/session/use-session";
import { useSessionAgentctl } from "@/hooks/domains/session/use-session-agentctl";
@@ -277,17 +277,11 @@ export function PassthroughTerminal(props: PassthroughTerminalProps) {
</div>
<TerminalSearchBar search={search} />
{!isConnected && (
<div
data-testid="passthrough-loading"
className="absolute inset-0 flex items-start justify-center pt-12 bg-background"
>
<div className="flex flex-col items-center gap-3 text-muted-foreground">
<GridSpinner />
<span className="text-sm">
{mode === "agent" ? "Preparing workspace..." : "Connecting terminal..."}
</span>
</div>
</div>
<PanelLoadingState
testId="passthrough-loading"
className="absolute inset-0 bg-background"
label={mode === "agent" ? "Preparing workspace..." : "Connecting terminal..."}
/>
)}
</div>
);
@@ -1,10 +1,10 @@
"use client";
import { useCallback, useMemo } from "react";
import { IconLoader2 } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { AgentLogo } from "@/components/agent-logo";
import { GridSpinner } from "@/components/grid-spinner";
import { PanelLoadingState } from "@/components/panel-loading-state";
import { SessionTabs, type SessionTab } from "@/components/session-tabs";
import { useAppStore } from "@/components/state-provider";
import { useToast } from "@/components/toast-provider";
@@ -177,15 +177,7 @@ function RunningSpinner() {
}
function PreviewLoadingState({ label }: { label: string }) {
return (
<div
className="flex h-full items-center justify-center gap-2 text-sm text-muted-foreground"
data-testid="preview-loading-state"
>
<IconLoader2 className="h-4 w-4 animate-spin" />
{label}
</div>
);
return <PanelLoadingState testId="preview-loading-state" label={label} />;
}
function PreviewEmptyState() {