From 6b857d256dfdc6adbd216e34bfdee53e55236e4b Mon Sep 17 00:00:00 2001 From: Dinesh Pandiyan Date: Mon, 19 Oct 2026 16:50:12 +1100 Subject: [PATCH 1/2] feat: show workspace skeletons while loading reviews --- DESIGN.md | 18 ++ apps/web/src/DiffWorkspace.tsx | 47 ++-- apps/web/src/Navigation.tsx | 13 +- apps/web/src/ProjectsNav.tsx | 30 ++- apps/web/src/WorkspaceLoading.tsx | 124 +++++++++ apps/web/src/components/ui/skeleton.tsx | 13 + apps/web/src/loading.css | 238 ++++++++++++++++++ apps/web/src/main.tsx | 1 + apps/web/src/session-context.tsx | 120 ++++----- apps/web/test/browser/local-workspace.spec.ts | 44 ++++ docs/development.md | 7 +- 11 files changed, 562 insertions(+), 93 deletions(-) create mode 100644 apps/web/src/WorkspaceLoading.tsx create mode 100644 apps/web/src/components/ui/skeleton.tsx create mode 100644 apps/web/src/loading.css diff --git a/DESIGN.md b/DESIGN.md index 00c96746..e53fceca 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -479,6 +479,24 @@ such detail into a token. New layout spacing must use the scale. Do not add motion for decoration. New animation must honor `prefers-reduced-motion`; feedback must still work without animation. +### Loading + +- Keep the real shell, Repos navigation and theme actions available while review + data loads. Reserve the shared navigation panel width and split/unified layout + with neutral skeletons for repo rows, file rows, the toolbar and diff frames. + Keep Diff disabled until a review is ready; switching to Repos replaces the file + placeholders within the same panel. + Narrow screens hide the placeholder file sidebar and follow the narrow layout. +- Skeletons use `--border-muted`, existing spacing and radius tokens, and a gentle + shared opacity pulse. Disable animation under `prefers-reduced-motion`. Keep + placeholders out of the accessibility tree and tab order; label the busy region + with a concise status at the bottom of the review surface. Do not invent file + names, counts, progress percentages or addition/deletion colors. +- Replace skeletons as data arrives, without a minimum display delay. Stored + manifest loading uses the same diff-frame skeleton without unmounting Pierre. + Background Sync preserves the existing review and usable controls. Errors and + empty results replace placeholders with their existing explanation and actions. + ## Responsive design - Use 1012px for drawer navigation/tighter gutters and 768px for compact mobile diff --git a/apps/web/src/DiffWorkspace.tsx b/apps/web/src/DiffWorkspace.tsx index 44fb1d29..fa9b7df8 100644 --- a/apps/web/src/DiffWorkspace.tsx +++ b/apps/web/src/DiffWorkspace.tsx @@ -1,3 +1,4 @@ +import { DiffSkeleton } from "./WorkspaceLoading.tsx"; import type { CodeViewItem, DiffLineAnnotation, @@ -627,10 +628,9 @@ export function DiffWorkspace() { ], ); + const loadingChanges = !repository && !diff.notice; const emptyTitle = !repository - ? diff.notice - ? "Cannot load changes" - : "Loading changes" + ? "Cannot load changes" : filter ? "No matching files" : piped @@ -639,9 +639,7 @@ export function DiffWorkspace() { ? "Nothing staged" : "No changes in this snapshot"; const emptyDescription = !repository - ? diff.notice - ? "Check that the server is running, then refresh." - : "Reading stored changes…" + ? "Check that the server is running, then refresh." : filter ? "Try a different filename or clear the filter." : piped @@ -795,17 +793,32 @@ export function DiffWorkspace() { } /> - diff --git a/apps/web/src/Navigation.tsx b/apps/web/src/Navigation.tsx index addf821b..c4ffc80f 100644 --- a/apps/web/src/Navigation.tsx +++ b/apps/web/src/Navigation.tsx @@ -1,18 +1,27 @@ import { useRef, type ReactNode } from "react"; +import { FileTreeSkeleton } from "./WorkspaceLoading.tsx"; import { ActivityBar } from "./ActivityBar.tsx"; import { ProjectsNav } from "./ProjectsNav.tsx"; import { useSidebarState } from "./sidebar-context.tsx"; /** The shared navigation frame owns panel width and its resize boundary. */ -export function Navigation({ children }: { children?: ReactNode }) { +export function Navigation({ + children, + loading = false, +}: { + children?: ReactNode; + loading?: boolean; +}) { const sidebar = useSidebarState(); const diffReady = Boolean(children); - const expanded = sidebar.expanded && (sidebar.view !== "diff" || diffReady); + const expanded = + sidebar.expanded && (sidebar.view !== "diff" || diffReady || loading); return (
); + if (collecting || !loaded) + return ( +
+ +

+ {collecting ? "Collecting repos…" : "Loading repos…"} +

+
+ ); return ( -
-

- {collecting - ? "Collecting repos…" - : loaded - ? "No current changes" - : "Loading repos…"} +

+

No current changes

+

+ Agent hooks and diffx sync publish changed checkouts here.

- {loaded && !collecting && ( -

- Agent hooks and diffx sync publish changed checkouts here. -

- )}
); } diff --git a/apps/web/src/WorkspaceLoading.tsx b/apps/web/src/WorkspaceLoading.tsx new file mode 100644 index 00000000..72909e8f --- /dev/null +++ b/apps/web/src/WorkspaceLoading.tsx @@ -0,0 +1,124 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { useAppearance } from "./appearance-context.tsx"; +import { useSidebarState } from "./sidebar-context.tsx"; + +const lines = [ + "medium", + "long", + "short", + "long", + "medium", + "short", + "medium", + "long", + "short", + "medium", + "long", + "short", +]; + +/** Decorative placeholders never claim file identities, counts or change status. */ +export function DiffSkeleton() { + const { layout } = useAppearance(); + return ( + + ); +} + +export function FileTreeSkeleton() { + const sidebar = useSidebarState(); + return ( + + ); +} + +export function ProjectsSkeleton() { + return ( + + ); +} + +export function WorkspaceLoading({ collecting }: { collecting: boolean }) { + return ( + <> + + +
+

{collecting ? "Collecting latest changes" : "Loading review"}

+

{collecting ? "Reading checkouts…" : "Reading stored changes…"}

+
+ + ); +} diff --git a/apps/web/src/components/ui/skeleton.tsx b/apps/web/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..c9c593bf --- /dev/null +++ b/apps/web/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import type { ComponentProps } from "react"; +import { cn } from "@/lib/utils"; + +export function Skeleton({ className, ...props }: ComponentProps<"span">) { + return ( +